├── .gitcd ├── .github └── workflows │ └── codeql-analysis.yml ├── .gitignore ├── .readthedocs.yml ├── .travis.yml ├── .travis ├── clone-test-repo.sh ├── git-add-build.sh ├── git-reset.sh ├── setup-ssh.sh ├── tests │ ├── git-cd-clean.sh │ ├── git-cd-compare.sh │ ├── git-cd-finish.sh │ ├── git-cd-init-release-date.sh │ ├── git-cd-init-release-file.sh │ ├── git-cd-init.sh │ ├── git-cd-refresh.sh │ ├── git-cd-release-date.sh │ ├── git-cd-release-file.sh │ ├── git-cd-release.sh │ ├── git-cd-review.sh │ ├── git-cd-start.sh │ ├── git-cd-test.sh │ ├── git-cd-upgrade.sh │ └── git-cd-version.sh └── travis_deploy_key.enc ├── .vscode └── settings.json ├── LICENSE ├── README.rst ├── TODO.rst ├── changelogs ├── CHANGELOG-2.0.10.rst ├── CHANGELOG-2.0.11.rst ├── CHANGELOG-2.0.12.rst ├── CHANGELOG-2.0.13.rst ├── CHANGELOG-2.0.14.rst ├── CHANGELOG-2.0.15.rst ├── CHANGELOG-2.0.16.rst ├── CHANGELOG-2.0.7.rst ├── CHANGELOG-2.0.8.rst ├── CHANGELOG-2.0.9.rst ├── CHANGELOG-2.1.0.rst ├── CHANGELOG-2.1.1.rst ├── CHANGELOG-2.1.2.rst ├── CHANGELOG-2.1.3.rst ├── CHANGELOG-2.1.4.rst ├── CHANGELOG-2.1.5.rst ├── CHANGELOG-2.1.6.rst ├── CHANGELOG-2.1.7.rst ├── CHANGELOG-2.1.8.rst ├── CHANGELOG-2.2.0.rst ├── CHANGELOG-2.2.2.rst └── CHANGELOG-2.2.3.rst ├── docs ├── Makefile ├── conf.py ├── index.rst ├── make.bat └── requirements.txt ├── gitcd ├── __init__.py ├── app │ ├── __init__.py │ ├── clean.py │ ├── release.py │ └── upgrade.py ├── bin │ ├── __init__.py │ ├── console.py │ └── kivy.py ├── config │ ├── __init__.py │ └── defaults.py ├── exceptions.py ├── git │ ├── __init__.py │ ├── branch.py │ ├── exceptions.py │ ├── nullRepository.py │ ├── remote.py │ ├── repository.py │ ├── server │ │ ├── __init__.py │ │ ├── bitbucket.py │ │ ├── github.py │ │ └── gitlab.py │ └── tag.py ├── interface │ ├── __init__.py │ ├── cli │ │ ├── __init__.py │ │ ├── abstract.py │ │ ├── clean.py │ │ ├── compare.py │ │ ├── finish.py │ │ ├── init.py │ │ ├── refresh.py │ │ ├── release.py │ │ ├── review.py │ │ ├── start.py │ │ ├── status.py │ │ ├── test.py │ │ ├── upgrade.py │ │ └── version.py │ └── kivy │ │ ├── __init__.py │ │ ├── branchpanel.py │ │ ├── clean.py │ │ ├── infopanel.py │ │ ├── mainpanel.py │ │ ├── navigation.py │ │ ├── panel.py │ │ ├── tagpanel.py │ │ └── upgrade.py └── package.py ├── publish.sh ├── requirements-dev.txt ├── setup.py └── version.txt /.gitcd: -------------------------------------------------------------------------------- 1 | extraReleaseCommand: ./publish.sh 2 | feature: null 3 | master: master 4 | tag: v 5 | test: test 6 | versionScheme: version.txt 7 | versionType: file 8 | -------------------------------------------------------------------------------- /.github/workflows/codeql-analysis.yml: -------------------------------------------------------------------------------- 1 | # For most projects, this workflow file will not need changing; you simply need 2 | # to commit it to your repository. 3 | # 4 | # You may wish to alter this file to override the set of languages analyzed, 5 | # or to provide custom queries or build logic. 6 | # 7 | # ******** NOTE ******** 8 | # We have attempted to detect the languages in your repository. Please check 9 | # the `language` matrix defined below to confirm you have the correct set of 10 | # supported CodeQL languages. 11 | # 12 | name: "CodeQL" 13 | 14 | on: 15 | push: 16 | branches: [ master ] 17 | pull_request: 18 | # The branches below must be a subset of the branches above 19 | branches: [ master ] 20 | schedule: 21 | - cron: '33 4 * * 4' 22 | 23 | jobs: 24 | analyze: 25 | name: Analyze 26 | runs-on: ubuntu-latest 27 | permissions: 28 | actions: read 29 | contents: read 30 | security-events: write 31 | 32 | strategy: 33 | fail-fast: false 34 | matrix: 35 | language: [ 'python' ] 36 | # CodeQL supports [ 'cpp', 'csharp', 'go', 'java', 'javascript', 'python' ] 37 | # Learn more: 38 | # https://docs.github.com/en/free-pro-team@latest/github/finding-security-vulnerabilities-and-errors-in-your-code/configuring-code-scanning#changing-the-languages-that-are-analyzed 39 | 40 | steps: 41 | - name: Checkout repository 42 | uses: actions/checkout@v2 43 | 44 | # Initializes the CodeQL tools for scanning. 45 | - name: Initialize CodeQL 46 | uses: github/codeql-action/init@v1 47 | with: 48 | languages: ${{ matrix.language }} 49 | # If you wish to specify custom queries, you can do so here or in a config file. 50 | # By default, queries listed here will override any specified in a config file. 51 | # Prefix the list here with "+" to use these queries and those in the config file. 52 | # queries: ./path/to/local/query, your-org/your-repo/queries@main 53 | 54 | # Autobuild attempts to build any compiled languages (C/C++, C#, or Java). 55 | # If this step fails, then you should remove it and run the build manually (see below) 56 | - name: Autobuild 57 | uses: github/codeql-action/autobuild@v1 58 | 59 | # ℹ️ Command-line programs to run using the OS shell. 60 | # 📚 https://git.io/JvXDl 61 | 62 | # ✏️ If the Autobuild fails above, remove it and uncomment the following three lines 63 | # and modify them (or add more) to build your code if your project 64 | # uses a compiled language 65 | 66 | #- run: | 67 | # make bootstrap 68 | # make release 69 | 70 | - name: Perform CodeQL Analysis 71 | uses: github/codeql-action/analyze@v1 72 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | *$py.class 5 | 6 | # C extensions 7 | *.so 8 | 9 | # Distribution / packaging 10 | .Python 11 | env/ 12 | build/ 13 | develop-eggs/ 14 | dist/ 15 | downloads/ 16 | eggs/ 17 | .eggs/ 18 | lib/ 19 | lib64/ 20 | parts/ 21 | sdist/ 22 | var/ 23 | *.egg-info/ 24 | .installed.cfg 25 | *.egg 26 | 27 | # PyInstaller 28 | # Usually these files are written by a python script from a template 29 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 30 | *.manifest 31 | *.spec 32 | 33 | # Installer logs 34 | pip-log.txt 35 | pip-delete-this-directory.txt 36 | 37 | # Unit test / coverage reports 38 | htmlcov/ 39 | .tox/ 40 | .coverage 41 | .coverage.* 42 | .cache 43 | nosetests.xml 44 | coverage.xml 45 | *,cover 46 | .hypothesis/ 47 | 48 | # Translations 49 | *.mo 50 | *.pot 51 | 52 | # Django stuff: 53 | *.log 54 | 55 | # Sphinx documentation 56 | docs/_build/ 57 | 58 | # PyBuilder 59 | target/ 60 | 61 | #Ipython Notebook 62 | .ipynb_checkpoints 63 | 64 | .gitcd-personal 65 | -------------------------------------------------------------------------------- /.readthedocs.yml: -------------------------------------------------------------------------------- 1 | requirements_file: docs/requirements.txt -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: python 2 | branches: 3 | only: 4 | - master 5 | python: 6 | - "3.5" 7 | - "3.6" 8 | - "3.7" 9 | - "3.8" 10 | 11 | before_install: 12 | - sudo apt-get -qq update 13 | - sudo apt-get install -y expect 14 | 15 | # command to install dependencies 16 | install: 17 | - pip install -r requirements-dev.txt 18 | - pip install . 19 | script: 20 | # Run flake8 on all .py files in all subfolders 21 | - find . -name \*.py -exec flake8 {} + 22 | 23 | # Scan for known security issues 24 | - bandit -r . 25 | 26 | # run tests 27 | - | # default repo setup 28 | bash .travis/setup-ssh.sh 29 | - | 30 | bash .travis/tests/git-cd-version.sh 31 | - | 32 | bash .travis/clone-test-repo.sh 33 | - | # check git-cd upgrade 34 | bash .travis/tests/git-cd-upgrade.sh 35 | - | # default git-cd workflow 36 | bash .travis/tests/git-cd-init.sh 37 | - | 38 | bash .travis/tests/git-cd-start.sh 39 | - | 40 | bash .travis/git-add-build.sh 41 | - | 42 | bash .travis/tests/git-cd-refresh.sh 43 | - | 44 | bash .travis/tests/git-cd-review.sh 45 | - | 46 | bash .travis/tests/git-cd-finish.sh 47 | - | 48 | bash .travis/tests/git-cd-release.sh 49 | - | # git-cd test compare and clean 50 | bash .travis/tests/git-cd-test.sh 51 | - | 52 | bash .travis/tests/git-cd-compare.sh 53 | - | 54 | bash .travis/tests/git-cd-clean.sh 55 | - | # make a release by date format 56 | bash .travis/tests/git-cd-init-release-date.sh 57 | - | 58 | bash .travis/tests/git-cd-release-date.sh 59 | - | 60 | bash .travis/git-reset.sh 61 | - | # make a release by version file and extra command 62 | bash .travis/tests/git-cd-init-release-file.sh 63 | - | 64 | bash .travis/tests/git-cd-release-file.sh 65 | - | 66 | bash .travis/git-reset.sh 67 | 68 | 69 | env: 70 | global: 71 | - secure: "gh+8ceEarA3KYPg/Fg1vS52a8+KOdj//UwOL48ZfumOagXs2u8d3mSa61BVukroa34QKJa89UY318AtXU+LCeQk/w7SMUpL0bCqLxefdo2/DfJZqAiX74Ema5VYENi6mbRdzC9dS5rRbG04m7p3vWxahs2LPZF7J+NFZAdgZqyThbDYfb9Qa/lKunq86b60iUmlj+rfLGfRJLlm/o5Iet3SBq0ktUXflEjltWuPLNwgPPoxOITrFNZG+GPsATepKKWIweLr/jJIoSa1SpHCcxMuHrdsFPhpIFLffmCnVIORSZe4l6tbDhGntQjR4J7mhULT8uqrG92Nl6QoNBpEP4UYigI+9qLnVdQGvKznbps9M4udvGBjs4UvR91d5Kjhbr7upH+DzRuVzRxfNNCLF4j+p9Vr2uZbXoaZovGExVcraUbNJBUW/xlDHXntRzytQhomoaaBWheODrSAvBXUx0j3QlGJMcFhHdHQs5hX7EwRY2xb8Dzd83B0iqAMDizeXuQuObjIUKq0CzD1z7viaBUhDFgDyzWiWB7vKHb2L4Lxfwb0JK0cw81l9XkbsYB99DD2MaO78yR/DzYUjHXqi9EPSZslGYum4YY46gUrdoaUSqcN+pmbpdUfjIVPM/o6ap9j2mScHn3uUKDXn9qRTRecOEwleT9uNohKBRFjA0wM=" 72 | -------------------------------------------------------------------------------- /.travis/clone-test-repo.sh: -------------------------------------------------------------------------------- 1 | # setup ssh key for https://github.com/gitcd-io/travis-gitcd 2 | 3 | git clone git@github.com:gitcd-io/travis-gitcd.git ~/build/gitcd-io/travis-gitcd 4 | -------------------------------------------------------------------------------- /.travis/git-add-build.sh: -------------------------------------------------------------------------------- 1 | set -e 2 | 3 | # change workdir to travis-gitcd 4 | cd ~/build/gitcd-io/travis-gitcd 5 | 6 | echo travis-$TRAVIS_JOB_NUMBER >> README.rst 7 | git commit -m "Add current build number: $TRAVIS_JOB_NUMBER" . 8 | git push 9 | 10 | # change back to original workdir 11 | cd - 12 | -------------------------------------------------------------------------------- /.travis/git-reset.sh: -------------------------------------------------------------------------------- 1 | set -e 2 | 3 | # change workdir to travis-gitcd 4 | cd ~/build/gitcd-io/travis-gitcd 5 | 6 | git reset --hard 7 | 8 | # change back to original workdir 9 | cd - 10 | -------------------------------------------------------------------------------- /.travis/setup-ssh.sh: -------------------------------------------------------------------------------- 1 | # setup ssh key for https://github.com/gitcd-io/travis-gitcd 2 | declare -r SSH_FILE="$(mktemp -u $HOME/.ssh/XXXXX)" 3 | 4 | openssl aes-256-cbc \ 5 | -K $encrypted_a8b48c8ad6aa_key \ 6 | -iv $encrypted_a8b48c8ad6aa_iv \ 7 | -in ".travis/travis_deploy_key.enc" \ 8 | -out "$SSH_FILE" -d 9 | 10 | chmod 600 "$SSH_FILE" \ 11 | && printf "%s\n" \ 12 | "Host github.com" \ 13 | " IdentityFile $SSH_FILE" \ 14 | " LogLevel ERROR" >> ~/.ssh/config 15 | -------------------------------------------------------------------------------- /.travis/tests/git-cd-clean.sh: -------------------------------------------------------------------------------- 1 | set -e 2 | 3 | # change workdir to travis-gitcd 4 | cd ~/build/gitcd-io/travis-gitcd 5 | 6 | # assert that new feature branch exists remote 7 | git branch -a | grep "origin/feature/travis-test-$TRAVIS_JOB_NUMBER" 8 | 9 | # delete feature branch on remote 10 | git push origin :feature/travis-test-$TRAVIS_JOB_NUMBER 11 | 12 | # assert that the feature branch not present on remote anymore 13 | REMOTE_COUNT=`git branch -a | grep "origin/feature/travis-test-$TRAVIS_JOB_NUMBER" | wc -l` 14 | if [ $REMOTE_COUNT != 0 ]; then 15 | echo "Feature Branch still found on remote" 16 | exit 1 17 | fi 18 | 19 | # call git-cd clean 20 | /usr/bin/expect < version.txt 8 | 9 | # git-cd init with accepting all the defaults 10 | /usr/bin/expect < done.txt\n" 26 | expect "Do you want to execute some additionalcommands before a release?" 27 | send "\n" 28 | expect 29 | EOD 30 | 31 | cat .gitcd | grep "versionType: file" 32 | 33 | # change back to original workdir 34 | cd - 35 | -------------------------------------------------------------------------------- /.travis/tests/git-cd-init.sh: -------------------------------------------------------------------------------- 1 | set -e 2 | 3 | # change workdir to travis-gitcd 4 | cd ~/build/gitcd-io/travis-gitcd 5 | 6 | # git-cd init with accepting all the defaults 7 | /usr/bin/expect < with your currently installed python version, e.g. `3.11` ** 99 | 100 | .. code-block:: bash 101 | 102 | if [ -d "$HOME/Library/Python//bin" ] ; then 103 | PATH="$HOME/Library/Python//bin:$PATH" 104 | fi 105 | 106 | Ubuntu / Debian 107 | _______________ 108 | 109 | Open ~/.profile in your favorite editor and add the following lines at the end of the file. 110 | 111 | .. code-block:: bash 112 | 113 | if [ -d "$HOME/.local/bin" ] ; then 114 | PATH="$HOME/.local/bin:$PATH" 115 | fi 116 | 117 | 118 | Argument Completion 119 | ------------------- 120 | Gitcd supports argument completion, to activate it execute the following steps. 121 | 122 | MacOSX 123 | ______ 124 | 125 | Under OSX it isn't that simple unfortunately. Global completion requires bash support for complete -D, which was introduced in bash 4.2. On OS X or older Linux systems, you will need to update bash to use this feature. Check the version of the running copy of bash with echo $BASH_VERSION. On OS X, install bash via Homebrew (brew install bash), add /usr/local/bin/bash to /etc/shells, and run chsh to change your shell. 126 | 127 | You might consider reading the docs for argcomplete https://argcomplete.readthedocs.io/en/latest/#global-completion 128 | 129 | Activate Global argcomplete 130 | _____________________________ 131 | 132 | You are now ready to activate global argcompletion for python with the following command. 133 | 134 | .. code-block:: bash 135 | 136 | activate-global-python-argcomplete 137 | 138 | 139 | 140 | 141 | CLI Usage of gitcd 142 | ################## 143 | 144 | For convenience, you can call gitcd as a git sub command as well as directly. Therefore, you can replace "git cd" in any of the following commands with "git-cd" if you like it more. 145 | 146 | 147 | .. container:: alert alert-warning 148 | 149 | **Note: Python argument completion wont work if you use it as a git sub command!** 150 | 151 | 152 | Initializing gitcd 153 | ------------------ 154 | First of all you probably want to initialize one of your local git repositories with gitcd. Change directory to one of your local git repositories and run git-cd init. 155 | Most of the values should be very self-explanatory. Still, here is a complete list of values you can pass. 156 | 157 | - **Branch name for production releases?** 158 | 159 | - This is the branch git-cd is creating a tag from if you execute the release command, you probably want to go with **master** here. 160 | 161 | - **Branch name for feature development?** 162 | 163 | - This is more kind of a prefix for feature branches, it is empty by default. If you wish your feature branch has a name like feature/my-new-feature, you can set this prefix to **feature/**. 164 | 165 | - **Branch name for test releases?** 166 | 167 | - Pass your branch name where you want to merge code into while executing git-cd test. Let it empty if you don't want to use that feature. At work, we have this for many repositories set to **test**. 168 | 169 | - **Version tag prefix?** 170 | 171 | - Prefix for your release tags, this is **v** by default which would result in a tag equals to v0.0.1 for example. 172 | 173 | - **Version type? You can either set your tag number manually, read it from a version file or generate it by date.** 174 | 175 | - This is about how git-cd release gets your current version number you want to release. 176 | 177 | - manual means you'll get asked to enter the version number by hand 178 | - file means gitcd reads the version number from a file, you'll be asked from which file in the next step 179 | - date means you generate a version number from a date scheme, you'll be asked for the scheme later. As a date version scheme, you can pass any directive for http://strftime.org/. 180 | 181 | - **Do you want to execute some additional commands after a release?** 182 | 183 | - This is useful if you want to execute any cli script after creating a tag, for example, gitcd itself uses such a script to publish the new release on pypi after creating a new tag. You can see the script here https://github.com/gitcd-io/gitcd/blob/master/publish.sh. 184 | 185 | - **Do you want to execute some additional commands before a release?** 186 | 187 | - This is useful if you want to execute any cli script before creating a tag, for example, if you want to modify any file in your git tree where you want to add the current version number. 188 | 189 | 190 | .. code-block:: bash 191 | 192 | git cd init 193 | 194 | The image below represents the configuration for gitcd itself. 195 | 196 | .. container:: responsive-image 197 | 198 | .. image:: https://www.gitcd.io/images/cli/git-cd_init.png 199 | :alt: git cd init 200 | 201 | 202 | 203 | Check current version 204 | --------------------- 205 | You want to know which version of gitcd you are currently running? 206 | 207 | .. code-block:: bash 208 | 209 | git cd version 210 | 211 | .. container:: responsive-image 212 | 213 | .. image:: https://www.gitcd.io/images/cli/git-cd_version.png 214 | :alt: git cd version 215 | 216 | 217 | Upgrade gitcd itself 218 | -------------------- 219 | Gitcd is able to check your local version with the one published on pypi and upgrade itself if you wish so. 220 | 221 | .. code-block:: bash 222 | 223 | git cd upgrade 224 | 225 | .. container:: responsive-image 226 | 227 | .. image:: https://www.gitcd.io/images/cli/git-cd_upgrade-2.png 228 | :alt: git cd upgrade 229 | 230 | 231 | Clean up local branches 232 | ----------------------- 233 | The tool is able to cleanup all local branches which doesn't exist on remotes. This is done with the clean command. 234 | 235 | .. code-block:: bash 236 | 237 | git cd clean 238 | 239 | .. container:: responsive-image 240 | 241 | .. image:: https://www.gitcd.io/images/cli/git-cd_clean-2.png 242 | :alt: git cd clean 243 | 244 | 245 | Start a new feature 246 | ------------------- 247 | Starts a new feature branch from your master branch. If you don't pass a branch name, you will be asked later. 248 | 249 | .. code-block:: bash 250 | 251 | git cd start 252 | 253 | .. container:: responsive-image 254 | 255 | .. image:: https://www.gitcd.io/images/cli/git-cd_start.png 256 | :alt: git cd start 257 | 258 | 259 | Updating a feature with the master branch 260 | ----------------------------------------- 261 | Merges the remote master branch into your current feature branch. If you don't pass a branch name, your current branch will be taken. 262 | 263 | .. code-block:: bash 264 | 265 | git cd refresh 266 | 267 | .. container:: responsive-image 268 | 269 | .. image:: https://www.gitcd.io/images/cli/git-cd_refresh.png 270 | :alt: git cd refresh 271 | 272 | 273 | Testing a feature 274 | ----------------- 275 | You might have a testing environment or want to run some integration test on a shared or common branch without the need to push out your feature with the next release. Therefore you can't merge it into the master. That's exactly why the git-cd test command exists. You might even have some dedicated tester checking the new feature on this specific branch. So to merge your new feature into your testing branch you call this command, if you don't pass a branch name, your current feature branch will be merged. 276 | 277 | .. code-block:: bash 278 | 279 | git cd test 280 | 281 | .. container:: responsive-image 282 | 283 | .. image:: https://www.gitcd.io/images/cli/git-cd_test.png 284 | :alt: git cd test 285 | 286 | 287 | Open a pull request for code review 288 | ----------------------------------- 289 | Opens a pull request to your master branch. If you don't pass a branch name, your current branch will be taken. 290 | 291 | .. code-block:: bash 292 | 293 | git cd review 294 | 295 | .. container:: responsive-image 296 | 297 | .. image:: https://www.gitcd.io/images/cli/git-cd_review.png 298 | :alt: git cd review 299 | 300 | 301 | See the status of a pull request 302 | -------------------------------- 303 | You can see the status of a pull request directly in the command line. If you don't pass a branch name, your current branch will be taken. 304 | 305 | .. code-block:: bash 306 | 307 | git cd status 308 | 309 | .. container:: responsive-image 310 | 311 | .. image:: https://www.gitcd.io/images/cli/git-cd_status.png 312 | :alt: git cd status 313 | 314 | Finish a feature branch 315 | ----------------------- 316 | If your pull request got approved by a fellow developer and all your tests were running properly, you probably want to merge your feature into the master branch. If you don't pass a branch name, your current branch will be taken. 317 | 318 | .. code-block:: bash 319 | 320 | git cd finish 321 | 322 | .. container:: responsive-image 323 | 324 | .. image:: https://www.gitcd.io/images/cli/git-cd_finish.png 325 | :alt: git cd finish 326 | 327 | 328 | Compare different branches or tags 329 | ---------------------------------- 330 | By now, your code is in the master branch. Personally, I always like to see what I am going to release by comparing the current branch (which is master after the finish) against the latest tag. If you don't pass a branch or tag name, the latest tag will be taken. 331 | 332 | .. code-block:: bash 333 | 334 | git cd compare || 335 | 336 | .. container:: responsive-image 337 | 338 | .. image:: https://www.gitcd.io/images/cli/git-cd_compare.png 339 | :alt: git cd compare 340 | 341 | 342 | Release a new version 343 | --------------------- 344 | Now your feature is merged and you made sure you know the changes going out, you are ready to ship it. This command creates a new tag from the master branch and executes any command you've setup in the initialize command. 345 | 346 | .. code-block:: bash 347 | 348 | git cd release 349 | 350 | .. container:: responsive-image 351 | 352 | .. image:: https://www.gitcd.io/images/cli/git-cd_release.png 353 | :alt: git cd release 354 | 355 | Known Issues 356 | ############ 357 | 358 | If you discover any bugs, feel free to create an issue on GitHub or fork this repository 359 | and send us a pull request. 360 | 361 | `Issues List`_. 362 | 363 | 364 | Authors 365 | ####### 366 | 367 | - Claudio Walser (https://github.com/claudio-walser) 368 | - Phil Christen (https://github.com/pchr-srf) 369 | - Urban Etter (https://github.com/mms-uret) 370 | - Gianni Carafa (https://github.com/mms-gianni) 371 | 372 | 373 | Contributing 374 | ############ 375 | 376 | 1. Fork it 377 | 2. Add this repository as an origin (``git remote add upstream https://github.com/gitcd-io/gitcd.git``) 378 | 3. Create your feature branch (``git cd start my-new-feature``) 379 | 4. Commit your changes (``git commit -am 'Add some feature'``) 380 | 5. Push to the branch (``git push origin feature/my-new-feature``) 381 | 6. Create new Pull Request against upstream (``git cd review my-new-feature``) 382 | 383 | 384 | License 385 | ####### 386 | 387 | Apache License 2.0 see 388 | https://github.com/gitcd-io/gitcd/blob/master/LICENSE 389 | 390 | .. _Issues List: https://github.com/gitcd-io/gitcd/issues 391 | 392 | -------------------------------------------------------------------------------- /TODO.rst: -------------------------------------------------------------------------------- 1 | Todo's and features to implement 2 | ================================ 3 | 4 | 5 | Features 6 | -------- 7 | 8 | * Check for updates initially on every command - not even sure if this is smart 9 | 10 | Completing tests 11 | ---------------- 12 | * implement all the assertions mentioned in the ./travis bash scripts 13 | * test it with different remotes if possible 14 | -------------------------------------------------------------------------------- /changelogs/CHANGELOG-2.0.10.rst: -------------------------------------------------------------------------------- 1 | Changelog for version 2.0.10 2 | ============================ 3 | 4 | Cleanup 5 | ####### 6 | 7 | - Moved changelog files into a separate folder 8 | - Removed unused package for travis setup -------------------------------------------------------------------------------- /changelogs/CHANGELOG-2.0.11.rst: -------------------------------------------------------------------------------- 1 | Changelog for version 2.0.11 2 | ============================ 3 | 4 | Cleanup 5 | ####### 6 | 7 | - Removed a pprint for debug in bitbucket integration -------------------------------------------------------------------------------- /changelogs/CHANGELOG-2.0.12.rst: -------------------------------------------------------------------------------- 1 | Changelog for version 2.0.12 2 | ============================ 3 | 4 | Security 5 | ######## 6 | 7 | - Added static source analyzer bandit from openstack for travis builds -------------------------------------------------------------------------------- /changelogs/CHANGELOG-2.0.13.rst: -------------------------------------------------------------------------------- 1 | Changelog for version 2.0.13 2 | ============================ 3 | 4 | Remote Update 5 | ############# 6 | 7 | - Simple git fetch -p instead of git remote update -p several times 8 | - Dropped support for python 3.3 -------------------------------------------------------------------------------- /changelogs/CHANGELOG-2.0.14.rst: -------------------------------------------------------------------------------- 1 | Changelog for version 2.0.14 2 | ============================ 3 | 4 | Gitlab Integration 5 | ################## 6 | 7 | - Implemented api calls to open pull requests on gitlab servers 8 | - Implemented api calls to see status of a pull request on gitlab 9 | - Both only work with repositories on gitlab.com official server, not on selfhosted gitlab servers -------------------------------------------------------------------------------- /changelogs/CHANGELOG-2.0.15.rst: -------------------------------------------------------------------------------- 1 | Changelog for version 2.0.15 2 | ============================ 3 | 4 | Pull requests from forks 5 | ######################## 6 | 7 | - Extended api calls to open pull requests on github from forks 8 | - Extended api calls to open pull requests on bitbucket from forks 9 | - Not able to get it work on gitlab forks yet -------------------------------------------------------------------------------- /changelogs/CHANGELOG-2.0.16.rst: -------------------------------------------------------------------------------- 1 | Changelog for version 2.0.16 2 | ============================ 3 | 4 | Flake8 cooding guidelines 5 | ######################### 6 | 7 | - Fixing flake8 issues for coding guidelines -------------------------------------------------------------------------------- /changelogs/CHANGELOG-2.0.7.rst: -------------------------------------------------------------------------------- 1 | Changelog for version 2.0.7 2 | =========================== 3 | 4 | Moved .gitcd-personal 5 | ##################### 6 | 7 | The personal config file including the token for opening pull requests on github was moved from your current repository into ~/.gitcd/access-token and prepared to store a bitbucket token as well. 8 | 9 | There is code to check if you don't have an access-token file yet in your home folder and if it finds a .gitcd-personal in your current repository, it will move it. I don't remove the entry for the file in .gitignore by intend, you may delete it manually if you are sure none of your teammates will commit his token by accident. I might do this by code in some month, still have to think about it. 10 | 11 | 12 | Improved docs 13 | ############# 14 | 15 | Docs where improved and adapted more closely with the online version on https://www.gitcd.io. Also screenshots are included on github and on readthedocs now. 16 | 17 | 18 | Introduced Changelogs 19 | ##################### 20 | 21 | I also decided to introduce changelogs to let you follow changes more closely. 22 | This is the first changelog and i hope you like it. -------------------------------------------------------------------------------- /changelogs/CHANGELOG-2.0.8.rst: -------------------------------------------------------------------------------- 1 | Changelog for version 2.0.8 2 | =========================== 3 | 4 | Bitbucket integration 5 | ##################### 6 | 7 | This was painful but it is finally done - gitcd's review and status commands work now with bitbucket as well as with github. 8 | 9 | \ 10 | 11 | Still to do 12 | ########### 13 | 14 | Unfortunately, the tool is still not able to open pull requests on upstream repositories for forks, I might tackle this in the upcoming release as well as gitlab integration - stay tuned. -------------------------------------------------------------------------------- /changelogs/CHANGELOG-2.0.9.rst: -------------------------------------------------------------------------------- 1 | Changelog for version 2.0.9 2 | =========================== 3 | 4 | Repository moved 5 | ################ 6 | 7 | Nothing to spectacular, repository was moved to gitcd-io organization within github -------------------------------------------------------------------------------- /changelogs/CHANGELOG-2.1.0.rst: -------------------------------------------------------------------------------- 1 | Changelog for version 2.1.0 2 | ============================ 3 | 4 | New command refresh 5 | ################### 6 | 7 | - Added new command git-cd refresh which is merging the master branch into your current feature branch 8 | - Reverse test branches if multiple found, that way, the most recent one appears as the first one -------------------------------------------------------------------------------- /changelogs/CHANGELOG-2.1.1.rst: -------------------------------------------------------------------------------- 1 | Changelog for version 2.1.1 2 | ============================ 3 | 4 | Check if a pull request is already open 5 | ####################################### 6 | 7 | - Check if a pull request is already open to prevent errors -------------------------------------------------------------------------------- /changelogs/CHANGELOG-2.1.2.rst: -------------------------------------------------------------------------------- 1 | Changelog for version 2.1.2 2 | ============================ 3 | 4 | Make git-cd work in subdirectories of your git tree 5 | ################################################### 6 | 7 | - git-cd now works in subdirectories of a git repository as well 8 | - slightly better exception handling -------------------------------------------------------------------------------- /changelogs/CHANGELOG-2.1.3.rst: -------------------------------------------------------------------------------- 1 | Changelog for version 2.1.3 2 | ============================ 3 | 4 | Upgrading git-cd it self is broken 5 | ################################## 6 | 7 | - git-cd is now able again to upgrade it self -------------------------------------------------------------------------------- /changelogs/CHANGELOG-2.1.4.rst: -------------------------------------------------------------------------------- 1 | Changelog for version 2.1.4 2 | ============================ 3 | 4 | Upgrade remote for git-cd refresh command 5 | ######################################### 6 | 7 | - git-cd refresh is now doing a remote update first 8 | - added python 3.7 for travis tests -------------------------------------------------------------------------------- /changelogs/CHANGELOG-2.1.5.rst: -------------------------------------------------------------------------------- 1 | Changelog for version 2.1.5 2 | ============================ 3 | 4 | Command to show current version of git-cd 5 | ######################################### 6 | 7 | - git-cd version is showing you the currently installed version -------------------------------------------------------------------------------- /changelogs/CHANGELOG-2.1.6.rst: -------------------------------------------------------------------------------- 1 | Changelog for version 2.1.6 2 | ============================ 3 | 4 | Docs for git-cd version command 5 | ############################### 6 | 7 | - git-cd version is even documented -------------------------------------------------------------------------------- /changelogs/CHANGELOG-2.1.7.rst: -------------------------------------------------------------------------------- 1 | Changelog for version 2.1.7 2 | ============================ 3 | 4 | Adding changelogs for all missed releases 5 | ######################################### 6 | 7 | - Added proper changelogs for all releases I missed to in the past -------------------------------------------------------------------------------- /changelogs/CHANGELOG-2.1.8.rst: -------------------------------------------------------------------------------- 1 | Changelog for version 2.1.8 2 | ============================ 3 | 4 | Better wording if multiple origins in use 5 | ######################################### 6 | 7 | - Proper wording for git-cd status with multiple remotes 8 | - Proper wording for git-cd review with multiple remotes -------------------------------------------------------------------------------- /changelogs/CHANGELOG-2.2.0.rst: -------------------------------------------------------------------------------- 1 | Changelog for version 2.2.0 2 | ============================ 3 | 4 | Try merge again if an error happend the first time 5 | ################################################## 6 | 7 | - git-cd test will resume if you like after a merge conflict 8 | - git-cd finish will resume if you like after a merge conflict 9 | - git-cd refresh will resume if you like after a merge conflict -------------------------------------------------------------------------------- /changelogs/CHANGELOG-2.2.2.rst: -------------------------------------------------------------------------------- 1 | Changelog for version 2.2.2 2 | ============================ 3 | 4 | Easy new branch opening 5 | ######################## 6 | 7 | - Replace spaces in branch name with dashes when a new branch is started -------------------------------------------------------------------------------- /changelogs/CHANGELOG-2.2.3.rst: -------------------------------------------------------------------------------- 1 | Changelog for version 2.2.3 2 | ============================ 3 | 4 | Execute pre release command 5 | ########################### 6 | 7 | - Adds the ability to execute any script before tagging 8 | - Renames the default branch from master to main -------------------------------------------------------------------------------- /docs/Makefile: -------------------------------------------------------------------------------- 1 | # Minimal makefile for Sphinx documentation 2 | # 3 | 4 | # You can set these variables from the command line. 5 | SPHINXOPTS = 6 | SPHINXBUILD = python -msphinx 7 | SPHINXPROJ = Gitcd 8 | SOURCEDIR = . 9 | BUILDDIR = _build 10 | 11 | # Put it first so that "make" without argument is like "make help". 12 | help: 13 | @$(SPHINXBUILD) -M help "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) 14 | 15 | .PHONY: help Makefile 16 | 17 | # Catch-all target: route all unknown targets to Sphinx using the new 18 | # "make mode" option. $(O) is meant as a shortcut for $(SPHINXOPTS). 19 | %: Makefile 20 | @$(SPHINXBUILD) -M $@ "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) -------------------------------------------------------------------------------- /docs/conf.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # 3 | # Gitcd documentation build configuration file, created by 4 | # sphinx-quickstart on Sat Oct 21 14:53:29 2017. 5 | # 6 | # This file is execfile()d with the current directory set to its 7 | # containing dir. 8 | # 9 | # Note that not all possible configuration values are present in this 10 | # autogenerated file. 11 | # 12 | # All configuration values have a default; values that are commented out 13 | # serve to show the default. 14 | 15 | # If extensions (or modules to document with autodoc) are in another directory, 16 | # add these directories to sys.path here. If the directory is relative to the 17 | # documentation root, use os.path.abspath to make it absolute, like shown here. 18 | # 19 | # import os 20 | # import sys 21 | # sys.path.insert(0, os.path.abspath('.')) 22 | 23 | 24 | # -- General configuration ------------------------------------------------ 25 | 26 | # If your documentation needs a minimal Sphinx version, state it here. 27 | # 28 | needs_sphinx = '1.6' 29 | 30 | # Add any Sphinx extension module names here, as strings. They can be 31 | # extensions coming with Sphinx (named 'sphinx.ext.*') or your custom 32 | # ones. 33 | extensions = [] 34 | 35 | # Add any paths that contain templates here, relative to this directory. 36 | templates_path = ['_templates'] 37 | 38 | # The suffix(es) of source filenames. 39 | # You can specify multiple suffix as a list of string: 40 | # 41 | # source_suffix = ['.rst', '.md'] 42 | source_suffix = '.rst' 43 | 44 | # The master toctree document. 45 | master_doc = 'index' 46 | 47 | # General information about the project. 48 | project = u'Gitcd' 49 | copyright = u'2017, Claudio Walser' 50 | author = u'Claudio Walser' 51 | 52 | # The version info for the project you're documenting, acts as replacement for 53 | # |version| and |release|, also used in various other places throughout the 54 | # built documents. 55 | # 56 | # The short X.Y version. 57 | version = u'1.6' 58 | # The full version, including alpha/beta/rc tags. 59 | release = u'1.6.16' 60 | 61 | # The language for content autogenerated by Sphinx. Refer to documentation 62 | # for a list of supported languages. 63 | # 64 | # This is also used if you do content translation via gettext catalogs. 65 | # Usually you set "language" from the command line for these cases. 66 | language = None 67 | 68 | # List of patterns, relative to source directory, that match files and 69 | # directories to ignore when looking for source files. 70 | # This patterns also effect to html_static_path and html_extra_path 71 | exclude_patterns = ['_build', 'Thumbs.db', '.DS_Store'] 72 | 73 | # The name of the Pygments (syntax highlighting) style to use. 74 | pygments_style = 'sphinx' 75 | 76 | # If true, `todo` and `todoList` produce output, else they produce nothing. 77 | todo_include_todos = False 78 | 79 | 80 | # -- Options for HTML output ---------------------------------------------- 81 | 82 | # The theme to use for HTML and HTML Help pages. See the documentation for 83 | # a list of builtin themes. 84 | # 85 | html_theme = 'sphinx_rtd_theme' 86 | 87 | # Theme options are theme-specific and customize the look and feel of a theme 88 | # further. For a list of options available for each theme, see the 89 | # documentation. 90 | # 91 | # html_theme_options = {} 92 | 93 | # Add any paths that contain custom static files (such as style sheets) here, 94 | # relative to this directory. They are copied after the builtin static files, 95 | # so a file named "default.css" will overwrite the builtin "default.css". 96 | html_static_path = ['_static'] 97 | 98 | # Custom sidebar templates, must be a dictionary that maps document names 99 | # to template names. 100 | # 101 | # This is required for the alabaster theme 102 | # refs: http://alabaster.readthedocs.io/en/latest/installation.html#sidebars 103 | html_sidebars = { 104 | '**': [ 105 | 'about.html', 106 | 'navigation.html', 107 | 'relations.html', # needs 'show_related': True theme option to display 108 | 'searchbox.html', 109 | 'donate.html', 110 | ] 111 | } 112 | 113 | 114 | # -- Options for HTMLHelp output ------------------------------------------ 115 | 116 | # Output file base name for HTML help builder. 117 | htmlhelp_basename = 'Gitcddoc' 118 | 119 | 120 | # -- Options for LaTeX output --------------------------------------------- 121 | 122 | latex_elements = { 123 | # The paper size ('letterpaper' or 'a4paper'). 124 | # 125 | # 'papersize': 'letterpaper', 126 | 127 | # The font size ('10pt', '11pt' or '12pt'). 128 | # 129 | # 'pointsize': '10pt', 130 | 131 | # Additional stuff for the LaTeX preamble. 132 | # 133 | # 'preamble': '', 134 | 135 | # Latex figure (float) alignment 136 | # 137 | # 'figure_align': 'htbp', 138 | } 139 | 140 | # Grouping the document tree into LaTeX files. List of tuples 141 | # (source start file, target name, title, 142 | # author, documentclass [howto, manual, or own class]). 143 | latex_documents = [ 144 | (master_doc, 'Gitcd.tex', u'Gitcd Documentation', 145 | u'Claudio Walser', 'manual'), 146 | ] 147 | 148 | 149 | # -- Options for manual page output --------------------------------------- 150 | 151 | # One entry per manual page. List of tuples 152 | # (source start file, name, description, authors, manual section). 153 | man_pages = [ 154 | (master_doc, 'gitcd', u'Gitcd Documentation', 155 | [author], 1) 156 | ] 157 | 158 | 159 | # -- Options for Texinfo output ------------------------------------------- 160 | 161 | # Grouping the document tree into Texinfo files. List of tuples 162 | # (source start file, target name, title, author, 163 | # dir menu entry, description, category) 164 | texinfo_documents = [ 165 | (master_doc, 'Gitcd', u'Gitcd Documentation', 166 | author, 'Gitcd', 'One line description of project.', 167 | 'Miscellaneous'), 168 | ] 169 | -------------------------------------------------------------------------------- /docs/index.rst: -------------------------------------------------------------------------------- 1 | .. Gitcd documentation master file, created by 2 | sphinx-quickstart on Sat Oct 21 14:53:29 2017. 3 | You can adapt this file completely to your liking, but it should at least 4 | contain the root `toctree` directive. 5 | 6 | Welcome to Gitcd's documentation! 7 | ================================= 8 | 9 | .. include:: ../README.rst 10 | .. include:: ../TODO.rst 11 | 12 | .. toctree:: 13 | :maxdepth: 2 14 | :caption: Contents: 15 | 16 | 17 | 18 | Indices and tables 19 | ================== 20 | 21 | * :ref:`genindex` 22 | * :ref:`modindex` 23 | * :ref:`search` 24 | -------------------------------------------------------------------------------- /docs/make.bat: -------------------------------------------------------------------------------- 1 | @ECHO OFF 2 | 3 | pushd %~dp0 4 | 5 | REM Command file for Sphinx documentation 6 | 7 | if "%SPHINXBUILD%" == "" ( 8 | set SPHINXBUILD=python -msphinx 9 | ) 10 | set SOURCEDIR=. 11 | set BUILDDIR=_build 12 | set SPHINXPROJ=Gitcd 13 | 14 | if "%1" == "" goto help 15 | 16 | %SPHINXBUILD% >NUL 2>NUL 17 | if errorlevel 9009 ( 18 | echo. 19 | echo.The Sphinx module was not found. Make sure you have Sphinx installed, 20 | echo.then set the SPHINXBUILD environment variable to point to the full 21 | echo.path of the 'sphinx-build' executable. Alternatively you may add the 22 | echo.Sphinx directory to PATH. 23 | echo. 24 | echo.If you don't have Sphinx installed, grab it from 25 | echo.http://sphinx-doc.org/ 26 | exit /b 1 27 | ) 28 | 29 | %SPHINXBUILD% -M %1 %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% 30 | goto end 31 | 32 | :help 33 | %SPHINXBUILD% -M help %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% 34 | 35 | :end 36 | popd 37 | -------------------------------------------------------------------------------- /docs/requirements.txt: -------------------------------------------------------------------------------- 1 | sphinx>=1.6.0 -------------------------------------------------------------------------------- /gitcd/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gitcd-io/gitcd/303ca1898eaeb7bdd704946cf2466d53b58ea0a0/gitcd/__init__.py -------------------------------------------------------------------------------- /gitcd/app/__init__.py: -------------------------------------------------------------------------------- 1 | from gitcd.config import Gitcd as GitcdConfig 2 | from gitcd.config import GitcdPersonal as GitcdPersonalConfig 3 | 4 | 5 | class App(object): 6 | config = GitcdConfig() 7 | configPersonal = GitcdPersonalConfig() 8 | -------------------------------------------------------------------------------- /gitcd/app/clean.py: -------------------------------------------------------------------------------- 1 | from gitcd.git.repository import Repository 2 | from gitcd.git.branch import Branch 3 | from gitcd.app import App 4 | 5 | 6 | class Clean(App): 7 | 8 | def __init__(self): 9 | self.repository = Repository() 10 | pass 11 | 12 | def getBranchesToDelete(self) -> [Branch]: 13 | remotes = self.repository.getRemotes() 14 | branches = self.repository.getBranches() 15 | 16 | branchesToDelete = [] 17 | 18 | for branch in branches: 19 | deleteBranch = True 20 | for remote in remotes: 21 | if remote.hasBranch(branch): 22 | deleteBranch = False 23 | 24 | if deleteBranch: 25 | branchesToDelete.append(branch) 26 | 27 | return branchesToDelete 28 | 29 | def deleteBranches(self, branches: [Branch] = []) -> bool: 30 | currentBranch = self.repository.getCurrentBranch() 31 | masterBranch = Branch(self.config.getMaster()) 32 | 33 | for branch in branches: 34 | if branch.getName() == currentBranch.getName(): 35 | currentBranch = self.repository.checkoutBranch(masterBranch) 36 | branch.delete() 37 | 38 | return True 39 | -------------------------------------------------------------------------------- /gitcd/app/release.py: -------------------------------------------------------------------------------- 1 | from typing import Union 2 | import time 3 | import os 4 | import simpcli 5 | 6 | from gitcd.git.repository import Repository 7 | from gitcd.git.branch import Branch 8 | from gitcd.git.remote import Remote 9 | from gitcd.git.tag import Tag 10 | 11 | from gitcd.exceptions import GitcdVersionFileNotFoundException 12 | from gitcd.app import App 13 | 14 | 15 | class Release(App): 16 | 17 | def checkout(self, remote: Remote, branch: Branch) -> bool: 18 | repository = Repository() 19 | repository.checkoutBranch(branch) 20 | remote.pull(branch) 21 | return True 22 | 23 | def readVersionFile(self, versionFile) -> Union[str, bool]: 24 | if not os.path.isfile(versionFile): 25 | raise GitcdVersionFileNotFoundException('Version file not found!') 26 | with open(versionFile, 'r') as f: 27 | return f.read().strip() 28 | 29 | def getVersion(self) -> Union[str, bool]: 30 | if self.config.getVersionType() == 'file': 31 | try: 32 | return self.readVersionFile( 33 | self.config.getVersionScheme() 34 | ) 35 | except GitcdVersionFileNotFoundException: 36 | return False 37 | elif self.config.getVersionType() == 'date': 38 | return time.strftime(self.config.getVersionScheme()) 39 | 40 | return False 41 | 42 | def release(self, version: str, message: str, remote: Remote) -> bool: 43 | preCommand = self.config.getPreReleaseCommand() 44 | if preCommand is not None: 45 | cli = simpcli.Command(True) 46 | cli.execute( 47 | preCommand 48 | ) 49 | 50 | tag = Tag(version) 51 | tag.create(message) 52 | remote.push(tag) 53 | extraCommand = self.config.getExtraReleaseCommand() 54 | if extraCommand is not None: 55 | cli = simpcli.Command(True) 56 | cli.execute( 57 | extraCommand 58 | ) 59 | 60 | return True 61 | -------------------------------------------------------------------------------- /gitcd/app/upgrade.py: -------------------------------------------------------------------------------- 1 | import simpcli 2 | from packaging import version 3 | import pkg_resources 4 | import requests 5 | 6 | from gitcd.app import App 7 | 8 | from gitcd.exceptions import GitcdPyPiApiException 9 | 10 | 11 | class Upgrade(App): 12 | 13 | localVersion = 0 14 | pypiVersion = 0 15 | packageUrl = 'https://pypi.org/pypi/gitcd/json' 16 | verboseCli = simpcli.Command(True) 17 | 18 | def getLocalVersion(self) -> str: 19 | self.localVersion = pkg_resources.get_distribution("gitcd").version 20 | 21 | return self.localVersion 22 | 23 | def getPypiVersion(self) -> str: 24 | response = requests.get( 25 | self.packageUrl 26 | ) 27 | 28 | if response.status_code != 200: 29 | raise GitcdPyPiApiException( 30 | "Could not fetch version info on PyPi site." + 31 | "You need to check manually, sorry for that." 32 | ) 33 | 34 | result = response.json() 35 | self.pypiVersion = result['info']['version'] 36 | 37 | return self.pypiVersion 38 | 39 | def isUpgradable(self) -> bool: 40 | if version.parse(self.localVersion) < version.parse(self.pypiVersion): 41 | return True 42 | return False 43 | 44 | def upgrade(self) -> bool: 45 | self.verboseCli.execute("pip3 install --user --upgrade gitcd") 46 | 47 | return True 48 | -------------------------------------------------------------------------------- /gitcd/bin/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gitcd-io/gitcd/303ca1898eaeb7bdd704946cf2466d53b58ea0a0/gitcd/bin/__init__.py -------------------------------------------------------------------------------- /gitcd/bin/console.py: -------------------------------------------------------------------------------- 1 | # PYTHON_ARGCOMPLETE_OK 2 | 3 | import sys 4 | import argcomplete 5 | import argparse 6 | 7 | from gitcd.interface.cli import Cli 8 | 9 | if len(sys.argv) == 2: 10 | # default branch name is * 11 | sys.argv.append('*') 12 | 13 | cli = Cli() 14 | 15 | # create parser in order to autocomplete 16 | parser = argparse.ArgumentParser() 17 | 18 | parser.add_argument( 19 | "command", 20 | help="Command to call.", 21 | type=str, 22 | choices=cli.getAvailableCommands() 23 | ) 24 | parser.add_argument( 25 | "branch", 26 | help="Your awesome feature-branch name", 27 | type=str 28 | ) 29 | argcomplete.autocomplete(parser) 30 | 31 | 32 | def main(): 33 | try: 34 | arguments = parser.parse_args() 35 | command = arguments.command 36 | branch = arguments.branch 37 | cli.dispatch(command, branch) 38 | sys.exit(0) 39 | except KeyboardInterrupt: 40 | cli.close("See you soon!") 41 | sys.exit(1) 42 | -------------------------------------------------------------------------------- /gitcd/bin/kivy.py: -------------------------------------------------------------------------------- 1 | from gitcd.interface.kivy import Kivy 2 | 3 | 4 | def main(): 5 | Kivy().run() 6 | -------------------------------------------------------------------------------- /gitcd/config/__init__.py: -------------------------------------------------------------------------------- 1 | import os 2 | import yaml 3 | 4 | from gitcd.config.defaults import GitcdDefaults 5 | from gitcd.config.defaults import GitcdPersonalDefaults 6 | from gitcd.exceptions import GitcdFileNotFoundException 7 | from gitcd.exceptions import GitcdTokenNotImplemented 8 | 9 | 10 | class Parser: 11 | 12 | yaml = {} 13 | 14 | def load(self, filename: str): 15 | # raise exception if no .gitcd in current working dir 16 | if not os.path.isfile(filename): 17 | raise GitcdFileNotFoundException("File %s not found" % filename) 18 | 19 | # open and load .gitcd 20 | with open(filename, 'r') as stream: 21 | self.yaml = yaml.safe_load(stream) 22 | 23 | return self.yaml 24 | 25 | def write(self, filename: str, config: dict): 26 | with open(filename, "w") as outfile: 27 | outfile.write(yaml.dump(config, default_flow_style=False)) 28 | 29 | 30 | class Gitcd: 31 | 32 | loaded = False 33 | filename = ".gitcd" 34 | parser = Parser() 35 | defaults = GitcdDefaults() 36 | config = {} 37 | 38 | def __init__(self): 39 | self.config = self.defaults.load() 40 | if os.path.isfile(self.filename): 41 | config = self.parser.load(self.filename) 42 | self.config.update(config) 43 | 44 | def getString(self, value): 45 | if not isinstance(value, str): 46 | return '' 47 | else: 48 | return value 49 | 50 | def setFilename(self, configFilename: str): 51 | self.filename = configFilename 52 | 53 | def write(self): 54 | self.parser.write(self.filename, self.config) 55 | 56 | def getMaster(self): 57 | return self.config['master'] 58 | 59 | def setMaster(self, master: str): 60 | self.config['master'] = master 61 | 62 | def getFeature(self): 63 | return self.config['feature'] 64 | 65 | def setFeature(self, feature: str): 66 | if feature == '': 67 | feature = None 68 | 69 | self.config['feature'] = feature 70 | 71 | def getTest(self): 72 | return self.config['test'] 73 | 74 | def setTest(self, test: str): 75 | if test == '': 76 | test = None 77 | 78 | self.config['test'] = test 79 | 80 | def getTag(self): 81 | return self.config['tag'] 82 | 83 | def setTag(self, tag: str): 84 | if tag == '': 85 | tag = None 86 | 87 | self.config['tag'] = tag 88 | 89 | def getVersionType(self): 90 | return self.config['versionType'] 91 | 92 | def setVersionType(self, versionType: str): 93 | self.config['versionType'] = versionType 94 | 95 | def getVersionScheme(self): 96 | return self.config['versionScheme'] 97 | 98 | def setVersionScheme(self, versionType: str): 99 | self.config['versionScheme'] = versionType 100 | 101 | def setExtraReleaseCommand(self, releaseCommand: str): 102 | if releaseCommand == '': 103 | releaseCommand = None 104 | self.config['extraReleaseCommand'] = releaseCommand 105 | 106 | def getExtraReleaseCommand(self): 107 | return self.config['extraReleaseCommand'] 108 | 109 | def setPreReleaseCommand(self, preReleaseCommand: str): 110 | if preReleaseCommand == '': 111 | preReleaseCommand = None 112 | self.config['preReleaseCommand'] = preReleaseCommand 113 | 114 | def getPreReleaseCommand(self): 115 | return self.config['preReleaseCommand'] 116 | 117 | 118 | class GitcdPersonal: 119 | 120 | loaded = False 121 | file = "access-tokens" 122 | path = os.path.expanduser('~/.gitcd') 123 | parser = Parser() 124 | defaults = GitcdPersonalDefaults() 125 | config = {} 126 | allowedTokenSpaces = ['github', 'bitbucket', 'gitlab'] 127 | 128 | def __init__(self): 129 | self.config = self.defaults.load() 130 | 131 | if not os.path.isdir(self.path): 132 | os.mkdir(self.path) 133 | 134 | self.filename = '%s/%s' % (self.path, self.file) 135 | 136 | if os.path.isfile(self.filename): 137 | config = self.parser.load(self.filename) 138 | self.config['tokens'].update(config['tokens']) 139 | 140 | def setFilename(self, configFilename: str): 141 | self.filename = configFilename 142 | 143 | def write(self): 144 | self.parser.write(self.filename, self.config) 145 | 146 | def getToken(self, tokenSpace: str): 147 | if tokenSpace not in self.allowedTokenSpaces: 148 | raise GitcdTokenNotImplemented( 149 | "Only tokens for '%s' are implemented!" % ( 150 | self.allowedTokenSpaces.join(', ') 151 | ) 152 | ) 153 | return self.config['tokens'][tokenSpace] 154 | 155 | def setToken(self, tokenSpace: str, token: str): 156 | if tokenSpace not in self.allowedTokenSpaces: 157 | raise GitcdTokenNotImplemented( 158 | "Only tokens for '%s' are implemented!" % ( 159 | self.allowedTokenSpaces.join(', ') 160 | ) 161 | ) 162 | self.config['tokens'][tokenSpace] = token 163 | 164 | 165 | class MoveGitcdPersonalPerRepo: 166 | 167 | filename = '.gitcd-personal' 168 | parser = Parser() 169 | config = {} 170 | 171 | def __init__(self): 172 | if os.path.isfile(self.filename): 173 | self.config = self.parser.load(self.filename) 174 | self.move() 175 | 176 | def move(self): 177 | newConfigFile = GitcdPersonal() 178 | if (not os.path.isfile(newConfigFile.filename) and 179 | 'token' in self.config and 180 | type(self.config['token']) is str): 181 | newConfigFile.setToken('github', self.config['token']) 182 | newConfigFile.write() 183 | os.remove(self.filename) 184 | 185 | # remove from .gitignore - not sure if this is smart 186 | # since the file could still be here for other users 187 | # and could therefore be commited by accident. 188 | # i may let this to the user 189 | # gitignore = ".gitignore" 190 | # if os.path.isfile(gitignore): 191 | # with open(gitignore, "r") as gitignoreFile: 192 | # gitignoreContent = gitignoreFile.read() 193 | 194 | # if gitignoreContent == self.filename: 195 | # os.remove(gitignore) 196 | # elif "%s" % (self.filename) not in gitignoreContent: 197 | # # remove it 198 | # gitignoreContent = gitignoreContent.replace( 199 | # "\n%s\n" % (self.filename), 200 | # '' 201 | # ) 202 | 203 | # with open(gitignore, "w") as gitignoreFile: 204 | # gitignoreFile.write(gitignoreContent) 205 | -------------------------------------------------------------------------------- /gitcd/config/defaults.py: -------------------------------------------------------------------------------- 1 | class GitcdDefaults(object): 2 | 3 | def load(self): 4 | return { 5 | 'test': None, 6 | 'feature': None, 7 | 'master': 'main', 8 | 'tag': None, 9 | 'versionType': 'manual', 10 | 'versionScheme': None, 11 | 'extraReleaseCommand': None, 12 | 'preReleaseCommand': None 13 | } 14 | 15 | 16 | class GitcdPersonalDefaults(object): 17 | 18 | def load(self): 19 | return { 20 | 'tokens': { 21 | 'github': None, 22 | 'bitbucket': None, 23 | 'gitlab': None 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /gitcd/exceptions.py: -------------------------------------------------------------------------------- 1 | class GitcdException(Exception): 2 | pass 3 | 4 | 5 | class GitcdTokenNotImplemented(GitcdException): 6 | pass 7 | 8 | 9 | class GitcdArgumentsException(GitcdException): 10 | pass 11 | 12 | 13 | class GitcdFileNotFoundException(GitcdException): 14 | pass 15 | 16 | 17 | class GitcdNoRepositoryException(GitcdException): 18 | pass 19 | 20 | 21 | class GitcdNoFeatureBranchException(GitcdException): 22 | pass 23 | 24 | 25 | class GitcdNoDevelopmentBranchDefinedException(GitcdException): 26 | pass 27 | 28 | 29 | class GitcdCliExecutionException(GitcdException): 30 | pass 31 | 32 | 33 | class GitcdGithubApiException(GitcdException): 34 | pass 35 | 36 | 37 | class GitcdPyPiApiException(GitcdException): 38 | pass 39 | 40 | 41 | class GitcdVersionFileNotFoundException(GitcdException): 42 | pass 43 | -------------------------------------------------------------------------------- /gitcd/git/__init__.py: -------------------------------------------------------------------------------- 1 | import simpcli 2 | 3 | from gitcd.config import Gitcd as GitcdConfig 4 | from gitcd.config import GitcdPersonal as GitcdPersonalConfig 5 | 6 | 7 | class Git(object): 8 | 9 | cli = simpcli.Command() 10 | verboseCli = simpcli.Command(True) 11 | config = GitcdConfig() 12 | configPersonal = GitcdPersonalConfig() 13 | 14 | def getConfig(self) -> GitcdConfig: 15 | return self.config 16 | 17 | def getPersonalConfig(self) -> GitcdPersonalConfig: 18 | return self.configPersonal 19 | -------------------------------------------------------------------------------- /gitcd/git/branch.py: -------------------------------------------------------------------------------- 1 | from gitcd.git import Git 2 | 3 | 4 | class Branch(Git): 5 | 6 | name = 'master' 7 | 8 | def __init__(self, name: str): 9 | self.name = name 10 | 11 | def getName(self) -> str: 12 | return self.name 13 | 14 | def isMaster(self) -> bool: 15 | return self.name == self.config.getMaster() 16 | 17 | def isTest(self) -> bool: 18 | testPrefix = self.config.getTest() 19 | if not testPrefix: 20 | return False 21 | 22 | return self.name.startswith(testPrefix) 23 | 24 | def isFeature(self) -> bool: 25 | if self.isMaster() or self.isTest(): 26 | return False 27 | 28 | if self.config.getFeature(): 29 | return self.name.startswith(self.config.getFeature()) 30 | return True 31 | 32 | def delete(self) -> bool: 33 | output = self.verboseCli.execute("git branch -D %s" % (self.name)) 34 | if output is False: 35 | return False 36 | return True 37 | -------------------------------------------------------------------------------- /gitcd/git/exceptions.py: -------------------------------------------------------------------------------- 1 | from gitcd.exceptions import GitcdException 2 | 3 | 4 | class NoRepositoryException(GitcdException): 5 | pass 6 | 7 | 8 | class RemoteNotFoundException(GitcdException): 9 | pass 10 | 11 | 12 | class BranchNotFoundException(GitcdException): 13 | pass 14 | 15 | 16 | class TagNotFoundException(GitcdException): 17 | pass 18 | -------------------------------------------------------------------------------- /gitcd/git/nullRepository.py: -------------------------------------------------------------------------------- 1 | from gitcd.git.repository import Repository 2 | 3 | 4 | class NullRepository(Repository): 5 | 6 | def __init__(self): 7 | self.directory = "" 8 | 9 | def getCurrentBranch(self) -> None: 10 | return None 11 | -------------------------------------------------------------------------------- /gitcd/git/remote.py: -------------------------------------------------------------------------------- 1 | from typing import Union 2 | 3 | from gitcd.git import Git 4 | from gitcd.git.server.github import Github 5 | from gitcd.git.server.bitbucket import Bitbucket 6 | from gitcd.git.server.gitlab import Gitlab 7 | from gitcd.git.branch import Branch 8 | from gitcd.git.tag import Tag 9 | 10 | 11 | class Remote(Git): 12 | 13 | name = 'origin' 14 | branches = [] 15 | tags = [] 16 | 17 | def __init__(self, name: str): 18 | self.name = name 19 | self.branches = [] 20 | self.tags = [] 21 | self.readRemoteConfig() 22 | 23 | def readRemoteConfig(self) -> bool: 24 | output = self.cli.execute('git config -l') 25 | 26 | if not output: 27 | return False 28 | 29 | lines = output.split("\n") 30 | url = False 31 | for line in lines: 32 | if line.startswith("remote.%s.url=" % (self.name)): 33 | lineParts = line.split("=") 34 | url = lineParts[1] 35 | 36 | # in case of https 37 | # https://github.com/claudio-walser/test-repo.git 38 | if url.startswith("https://") or url.startswith("http://"): 39 | url = url.replace("http://", "") 40 | url = url.replace("https://", "") 41 | # in case of ssh git@github.com:claudio-walser/test-repo.git 42 | else: 43 | urlParts = url.split("@") 44 | url = urlParts[1] 45 | url = url.replace(":", "/") 46 | 47 | self.url = url 48 | 49 | urlParts = url.split("/") 50 | self.username = urlParts[1] 51 | self.repositoryName = urlParts[-1] 52 | self.repositoryName = self.repositoryName.replace('.git', '') 53 | 54 | return True 55 | 56 | def getName(self) -> str: 57 | return self.name 58 | 59 | def getRepositoryName(self) -> str: 60 | return self.repositoryName 61 | 62 | def getUrl(self) -> str: 63 | return self.url 64 | 65 | def getUsername(self) -> str: 66 | return self.username 67 | 68 | def readBranches(self) -> bool: 69 | if self.branches: 70 | return True 71 | 72 | output = self.cli.execute('git branch -r') 73 | if not output: 74 | return False 75 | 76 | lines = output.split("\n") 77 | for line in lines: 78 | line = line.strip() 79 | if line.startswith('%s/' % (self.name)): 80 | self.branches.append(line.replace('%s/' % (self.name), '')) 81 | 82 | return True 83 | 84 | def readTags(self) -> bool: 85 | if self.tags: 86 | return True 87 | 88 | output = self.cli.execute('git ls-remote -t --refs %s' % self.name) 89 | if not output: 90 | return False 91 | 92 | lines = output.split("\n") 93 | for line in lines: 94 | line = line.strip() 95 | parts = line.split('refs/tags/') 96 | self.tags.append(parts[-1]) 97 | 98 | return True 99 | 100 | def hasBranch(self, branch: Branch) -> bool: 101 | self.readBranches() 102 | if branch.getName() in self.branches: 103 | return True 104 | 105 | return False 106 | 107 | def hasTag(self, tag: Tag) -> bool: 108 | self.readTags() 109 | if tag.getName() in self.tags: 110 | return True 111 | 112 | return False 113 | 114 | def push(self, branch: Union[Branch, Tag]) -> bool: 115 | self.verboseCli.execute( 116 | "git push %s %s" % (self.name, branch.getName()) 117 | ) 118 | if type(branch) is Branch: 119 | self.verboseCli.execute( 120 | "git branch --set-upstream-to %s/%s" % ( 121 | self.name, 122 | branch.getName() 123 | ) 124 | ) 125 | return True 126 | 127 | def pull(self, branch: Branch) -> bool: 128 | self.verboseCli.execute( 129 | 'git pull %s %s' % (self.name, branch.getName()) 130 | ) 131 | return True 132 | 133 | def delete(self, branch: Branch) -> bool: 134 | output = self.verboseCli.execute("git push %s :%s" % ( 135 | self.name, branch.getName()) 136 | ) 137 | if output is False: 138 | return False 139 | return True 140 | 141 | def isBehind(self, branch: Branch) -> bool: 142 | output = self.cli.execute( 143 | "git log %s/%s..%s" % ( 144 | self.name, 145 | branch.getName(), 146 | branch.getName() 147 | ) 148 | ) 149 | if not output: 150 | return False 151 | 152 | return True 153 | 154 | def createFeature(self, branch: Branch) -> Branch: 155 | self.verboseCli.execute( 156 | "git checkout %s" % (self.config.getMaster()) 157 | ) 158 | self.verboseCli.execute( 159 | "git pull %s %s" % (self.name, self.config.getMaster()) 160 | ) 161 | self.verboseCli.execute( 162 | "git checkout -b %s" % (branch.getName()) 163 | ) 164 | 165 | self.push(branch) 166 | 167 | return branch 168 | 169 | def merge(self, branch: Branch, branchToMerge: Branch) -> bool: 170 | self.verboseCli.execute("git checkout %s" % (branch.getName())) 171 | self.verboseCli.execute("git pull %s %s" % ( 172 | self.name, 173 | branch.getName() 174 | )) 175 | 176 | self.verboseCli.execute("git merge %s/%s" % ( 177 | self.name, 178 | branchToMerge.getName() 179 | )) 180 | 181 | self.push(branch) 182 | 183 | def compare(self, branch: Branch, toCompare: [Branch, Tag]) -> bool: 184 | if isinstance(toCompare, Tag): 185 | toCompareString = toCompare.getName() 186 | else: 187 | toCompareString = '%s/%s' % (self.name, toCompare.getName()) 188 | self.verboseCli.execute("git diff %s %s --color" % ( 189 | toCompareString, 190 | branch.getName() 191 | )) 192 | return True 193 | 194 | # Get PullRequest Implementation for either Github or Bitbucket 195 | def getGitWebIntegration(self) -> [Github, Bitbucket]: 196 | if self.isGithub(): 197 | pr = Github() 198 | elif self.isBitbucket(): 199 | pr = Bitbucket() 200 | elif self.isGitlab(): 201 | pr = Gitlab() 202 | else: 203 | # todo: raise RepoProviderNotImplementedException 204 | return False 205 | pr.setRemote(self) 206 | 207 | return pr 208 | 209 | def isGithub(self) -> bool: 210 | return 'github.com' in self.url 211 | 212 | def isBitbucket(self) -> bool: 213 | return 'bitbucket.org' in self.url 214 | 215 | def isGitlab(self) -> bool: 216 | return 'gitlab.com' in self.url 217 | -------------------------------------------------------------------------------- /gitcd/git/repository.py: -------------------------------------------------------------------------------- 1 | import os 2 | from typing import List 3 | 4 | from gitcd.git import Git 5 | from gitcd.git.branch import Branch 6 | from gitcd.git.remote import Remote 7 | from gitcd.git.tag import Tag 8 | 9 | # git exceptions 10 | from gitcd.git.exceptions import NoRepositoryException 11 | from gitcd.git.exceptions import RemoteNotFoundException 12 | from gitcd.git.exceptions import BranchNotFoundException 13 | from gitcd.git.exceptions import TagNotFoundException 14 | 15 | # default exceptions 16 | from gitcd.exceptions import GitcdNoDevelopmentBranchDefinedException 17 | 18 | 19 | class Repository(Git): 20 | 21 | directory = None 22 | name = None 23 | remotes = [] 24 | 25 | def __init__(self): 26 | self.directory = self.cli.execute('git rev-parse --show-toplevel') 27 | 28 | if self.directory is False: 29 | raise NoRepositoryException( 30 | 'No git repository found in %s' % ( 31 | os.getcwd() 32 | ) 33 | ) 34 | 35 | def getDirectory(self) -> str: 36 | return self.directory 37 | 38 | def getRemotes(self) -> List[Remote]: 39 | output = self.cli.execute('git remote') 40 | if not output: 41 | return [] 42 | 43 | lines = output.split("\n") 44 | 45 | remotes = [] 46 | for line in lines: 47 | line = line.strip() 48 | remotes.append(Remote(line)) 49 | 50 | return remotes 51 | 52 | def getRemote(self, remoteStr: str) -> Remote: 53 | remotes = self.getRemotes() 54 | for remote in remotes: 55 | if remote.getName() == remoteStr: 56 | return remote 57 | 58 | raise RemoteNotFoundException('Remote %s not found' % (remoteStr)) 59 | 60 | def getBranches(self) -> List[Branch]: 61 | output = self.cli.execute('git branch -a') 62 | if not output: 63 | return [] 64 | 65 | lines = output.split("\n") 66 | branches = [] 67 | for line in lines: 68 | line = line.strip() 69 | if 'HEAD -> ' in line: 70 | continue 71 | 72 | if not line.startswith('remotes/'): 73 | branch = line.replace('* ', '') 74 | elif line.startswith('remotes/'): 75 | parts = line.split('/') 76 | del parts[0] 77 | del parts[0] 78 | branch = '/'.join(parts) 79 | if branch not in branches: 80 | branches.append(branch) 81 | 82 | branchObjects = [] 83 | branches.sort() 84 | for branch in branches: 85 | branchObject = Branch(branch) 86 | branchObjects.append(branchObject) 87 | 88 | return branchObjects 89 | 90 | def getDevelopmentBranches(self) -> [Branch]: 91 | branches = self.getBranches() 92 | developmentBranches = [] 93 | for branch in branches: 94 | if branch.isTest(): 95 | developmentBranches.append(branch) 96 | 97 | if len(developmentBranches) < 1: 98 | raise GitcdNoDevelopmentBranchDefinedException( 99 | "No development branch found" 100 | ) 101 | return developmentBranches 102 | 103 | def getBranch(self, branchStr: str) -> Branch: 104 | branches = self.getBranches() 105 | for branch in branches: 106 | if branch.getName() == branchStr: 107 | return branch 108 | 109 | raise BranchNotFoundException('Branch %s not found' % (branchStr)) 110 | 111 | def getCurrentBranch(self) -> Branch: 112 | return Branch(self.cli.execute('git rev-parse --abbrev-ref HEAD')) 113 | 114 | def checkoutBranch(self, branch: Branch) -> Branch: 115 | self.verboseCli.execute('git checkout %s' % (branch.getName())) 116 | return branch 117 | 118 | def getTags(self) -> List[Tag]: 119 | output = self.cli.execute('git tag -l') 120 | if not output: 121 | return [] 122 | 123 | lines = output.split("\n") 124 | 125 | tags = [] 126 | for line in lines: 127 | tag = line.strip() 128 | 129 | if tag not in tags: 130 | tags.append(tag) 131 | 132 | tagObjects = [] 133 | tags.sort() 134 | for tag in tags: 135 | tagObject = Tag(tag) 136 | tagObjects.append(tagObject) 137 | 138 | return tagObjects 139 | 140 | def getTag(self, tagStr: str) -> Tag: 141 | tags = self.getTags() 142 | for tag in tags: 143 | if tag.getName() == tagStr: 144 | return tag 145 | 146 | raise TagNotFoundException('Tag %s not found' % (tagStr)) 147 | 148 | def getLatestTag(self) -> [Branch, Tag]: 149 | output = self.cli.execute("git describe --abbrev=0") 150 | if not output: 151 | return Branch(self.config.getMaster()) 152 | return Tag(output.strip()) 153 | 154 | def hasUncommitedChanges(self) -> bool: 155 | output = self.cli.execute("git status --porcelain") 156 | if not output: 157 | return False 158 | 159 | return True 160 | 161 | def update(self) -> bool: 162 | self.cli.execute('git fetch -p') 163 | 164 | return True 165 | -------------------------------------------------------------------------------- /gitcd/git/server/__init__.py: -------------------------------------------------------------------------------- 1 | from gitcd.git import Git 2 | from sys import platform 3 | 4 | 5 | class GitServer(Git): 6 | 7 | tokenSpace = None 8 | 9 | def getTokenSpace(self) -> str: 10 | return self.tokenSpace 11 | 12 | def setRemote(self, remote) -> bool: 13 | self.remote = remote 14 | return True 15 | 16 | def openBrowser(self, url: str) -> bool: 17 | defaultBrowser = self.getDefaultBrowserCommand() 18 | self.cli.execute("%s %s" % ( 19 | defaultBrowser, 20 | url 21 | )) 22 | return True 23 | 24 | def getDefaultBrowserCommand(self): 25 | if platform == "linux" or platform == "linux2": 26 | return "sensible-browser" 27 | elif platform == "darwin": 28 | return "open" 29 | elif platform == "win32": 30 | raise Exception("You have to be fucking kidding me") 31 | 32 | def open(self): 33 | raise Exception('Not implemented') 34 | 35 | def status(self): 36 | raise Exception('Not implemented') 37 | -------------------------------------------------------------------------------- /gitcd/git/server/bitbucket.py: -------------------------------------------------------------------------------- 1 | from gitcd.git.server import GitServer 2 | from gitcd.git.branch import Branch 3 | 4 | from gitcd.exceptions import GitcdGithubApiException 5 | 6 | import requests 7 | 8 | 9 | class Bitbucket(GitServer): 10 | 11 | tokenSpace = 'bitbucket' 12 | baseUrl = 'https://api.bitbucket.org/2.0' 13 | 14 | def getAuth(self): 15 | token = self.configPersonal.getToken(self.tokenSpace) 16 | if isinstance(token, str) and ':' in token: 17 | auth = token.split(':') 18 | return (auth[0], auth[1]) 19 | return None 20 | 21 | def open( 22 | self, 23 | title: str, 24 | body: str, 25 | fromBranch: Branch, 26 | toBranch: Branch, 27 | sourceRemote=None 28 | ) -> bool: 29 | auth = self.getAuth() 30 | if auth is not None: 31 | url = "%s/repositories/%s/%s/pullrequests" % ( 32 | self.baseUrl, 33 | self.remote.getUsername(), 34 | self.remote.getRepositoryName() 35 | ) 36 | data = { 37 | "destination": { 38 | "branch": { 39 | "name": toBranch.getName() 40 | } 41 | }, 42 | "source": { 43 | "branch": { 44 | "name": fromBranch.getName() 45 | } 46 | }, 47 | "title": title, 48 | "description": body 49 | } 50 | 51 | if sourceRemote is not None: 52 | if sourceRemote.isBitbucket() is not True: 53 | raise GitcdGithubApiException( 54 | "Bitbucket is not able to get a pr" + 55 | " from a different server" 56 | ) 57 | data['source']['repository'] = { 58 | 'full_name': "%s/%s" % ( 59 | sourceRemote.getUsername(), 60 | sourceRemote.getRepositoryName() 61 | ) 62 | } 63 | 64 | response = requests.post( 65 | url, 66 | json=data, 67 | auth=auth 68 | ) 69 | 70 | if response.status_code == 401: 71 | raise GitcdGithubApiException( 72 | "Authentication failed, create a new app password." 73 | ) 74 | 75 | if response.status_code != 201: 76 | try: 77 | jsonResponse = response.json() 78 | message = jsonResponse['error']['message'] 79 | raise GitcdGithubApiException( 80 | "Open a pull request on bitbucket" + 81 | " failed with message: %s" % ( 82 | message 83 | ) 84 | ) 85 | except ValueError: 86 | raise GitcdGithubApiException( 87 | "Open a pull request on bitbucket failed." 88 | ) 89 | 90 | defaultBrowser = self.getDefaultBrowserCommand() 91 | self.cli.execute("%s %s" % ( 92 | defaultBrowser, 93 | response.json()["links"]['html']['href'] 94 | )) 95 | else: 96 | defaultBrowser = self.getDefaultBrowserCommand() 97 | self.cli.execute("%s %s" % ( 98 | defaultBrowser, 99 | "%s/%s/%s/pull-requests/new?source=%s&event_source=gitcd" % ( 100 | "https://bitbucket.org", 101 | self.remote.getUsername(), 102 | self.remote.getRepositoryName(), 103 | fromBranch.getName() 104 | ) 105 | )) 106 | return True 107 | 108 | def status(self, branch: Branch, sourceRemote=None): 109 | master = Branch(self.config.getMaster()) 110 | auth = self.getAuth() 111 | if auth is not None: 112 | url = "%s/repositories/%s/%s/pullrequests" % ( 113 | self.baseUrl, 114 | self.remote.getUsername(), 115 | self.remote.getRepositoryName() 116 | ) 117 | 118 | response = requests.get( 119 | url, 120 | auth=auth 121 | ) 122 | 123 | if response.status_code != 200: 124 | raise GitcdGithubApiException( 125 | "Could not fetch open pull requests," + 126 | " please have a look manually." 127 | ) 128 | 129 | returnValue = {} 130 | responseJson = response.json() 131 | if 'values' in responseJson and len(responseJson['values']) > 0: 132 | for pr in responseJson['values']: 133 | if ( 134 | 'source' in pr and 135 | 'branch' in pr['source'] and 136 | 'name' in pr['source']['branch'] and 137 | pr['source']['branch']['name'] == branch.getName() 138 | ): 139 | currentPr = pr 140 | reviewers = self.isReviewedBy( 141 | currentPr['links']['activity']['href'] 142 | ) 143 | 144 | if len(reviewers) == 0: 145 | reviewers = self.getLgtmComments( 146 | currentPr['links']['comments']['href'] 147 | ) 148 | 149 | returnValue['state'] = 'REVIEW REQUIRED' 150 | 151 | if len(reviewers) > 0: 152 | returnValue['state'] = 'APPROVED' 153 | for reviewer in reviewers: 154 | reviewer = reviewers[reviewer] 155 | if reviewer['state'] != 'APPROVED': 156 | returnValue['state'] = reviewer['state'] 157 | 158 | returnValue['master'] = master.getName() 159 | returnValue['feature'] = branch.getName() 160 | returnValue['reviews'] = reviewers 161 | returnValue['url'] = currentPr['links']['html']['href'] 162 | returnValue['number'] = currentPr['id'] 163 | 164 | return returnValue 165 | 166 | def isReviewedBy(self, activityUrl: str) -> dict: 167 | auth = self.getAuth() 168 | if auth is not None: 169 | response = requests.get( 170 | activityUrl, 171 | auth=auth 172 | ) 173 | if response.status_code != 200: 174 | raise GitcdGithubApiException( 175 | "Fetch PR activity for bitbucket failed." 176 | ) 177 | 178 | responseJson = response.json() 179 | reviewers = {} 180 | if ('values' in responseJson): 181 | for value in responseJson['values']: 182 | if 'approval' in value: 183 | reviewer = {} 184 | reviewer['comments'] = [] 185 | approval = value['approval'] 186 | comment = {} 187 | comment['body'] = 'approved' 188 | comment['state'] = 'APPROVED' 189 | reviewer['state'] = 'APPROVED' 190 | reviewer['comments'].append(comment) 191 | 192 | reviewers[approval['user']['username']] = reviewer 193 | 194 | return reviewers 195 | 196 | def getLgtmComments(self, commentsUrl): 197 | auth = self.getAuth() 198 | reviewers = {} 199 | if auth is not None: 200 | response = requests.get( 201 | commentsUrl, 202 | auth=auth 203 | ) 204 | if response.status_code != 200: 205 | raise GitcdGithubApiException( 206 | "Fetch PR comments for bitbucket failed." 207 | ) 208 | 209 | comments = response.json() 210 | 211 | if 'values' in comments: 212 | for comment in comments['values']: 213 | if ( 214 | 'content' in comment and 215 | 'lgtm' in comment['content']['raw'].lower() 216 | ): 217 | if comment['user']['username'] in reviewers: 218 | reviewer = reviewers[comment['user']['username']] 219 | else: 220 | reviewer = {} 221 | reviewer['comments'] = [] 222 | 223 | reviewer['state'] = 'APPROVED' 224 | reviewerComment = {} 225 | reviewerComment['state'] = 'APPROVED' 226 | reviewerComment['body'] = comment['content']['raw'] 227 | reviewer['comments'].append(reviewerComment) 228 | reviewers[comment['user']['username']] = reviewer 229 | 230 | return reviewers 231 | -------------------------------------------------------------------------------- /gitcd/git/server/github.py: -------------------------------------------------------------------------------- 1 | from gitcd.git.server import GitServer 2 | from gitcd.git.branch import Branch 3 | 4 | from gitcd.exceptions import GitcdGithubApiException 5 | 6 | import json 7 | import requests 8 | 9 | 10 | class Github(GitServer): 11 | 12 | tokenSpace = 'github' 13 | baseUrl = 'https://api.github.com' 14 | 15 | def open( 16 | self, 17 | title: str, 18 | body: str, 19 | fromBranch: Branch, 20 | toBranch: Branch, 21 | sourceRemote=None 22 | ) -> bool: 23 | token = self.configPersonal.getToken(self.tokenSpace) 24 | url = "%s/repos/%s/%s/pulls" % ( 25 | self.baseUrl, 26 | self.remote.getUsername(), 27 | self.remote.getRepositoryName() 28 | ) 29 | head = '' 30 | if sourceRemote is not None: 31 | # check sourceRemote is github as well 32 | if sourceRemote.isGithub() is not True: 33 | raise GitcdGithubApiException( 34 | "Github is not able to get a pr from a different server" 35 | ) 36 | head = '%s:' % (sourceRemote.getUsername()) 37 | # check if the token is a string - does not necessarily mean its valid 38 | if isinstance(token, str): 39 | data = { 40 | "title": title, 41 | "body": body, 42 | "head": '%s%s' % (head, fromBranch.getName()), 43 | "base": toBranch.getName() 44 | } 45 | 46 | headers = {'Authorization': 'token %s' % token} 47 | response = requests.post( 48 | url, 49 | headers=headers, 50 | data=json.dumps(data), 51 | ) 52 | 53 | if response.status_code == 401: 54 | raise GitcdGithubApiException( 55 | "Authentication failed, create a new access token." 56 | ) 57 | 58 | if response.status_code != 201: 59 | try: 60 | jsonResponse = response.json() 61 | message = jsonResponse['message'] 62 | raise GitcdGithubApiException( 63 | "Open a pull request failed with message: %s" % ( 64 | message 65 | ) 66 | ) 67 | except ValueError: 68 | raise GitcdGithubApiException( 69 | "Open a pull request on github failed." 70 | ) 71 | 72 | defaultBrowser = self.getDefaultBrowserCommand() 73 | self.cli.execute("%s %s" % ( 74 | defaultBrowser, 75 | response.json()["html_url"] 76 | )) 77 | 78 | else: 79 | defaultBrowser = self.getDefaultBrowserCommand() 80 | self.cli.execute("%s %s" % ( 81 | defaultBrowser, 82 | "https://github.com/%s/%s/compare/%s...%s" % ( 83 | self.remote.getUsername(), 84 | self.remote.getRepositoryName(), 85 | toBranch.getName(), 86 | fromBranch.getName() 87 | ) 88 | )) 89 | return True 90 | 91 | def status(self, branch: Branch, sourceRemote=None): 92 | username = self.remote.getUsername() 93 | if sourceRemote is not None: 94 | # check sourceRemote is github as well 95 | if sourceRemote.isGithub() is not True: 96 | raise GitcdGithubApiException( 97 | "Github is not able to see a pr from a different server" 98 | ) 99 | ref = '%s:%s' % (sourceRemote.getUsername(), branch.getName()) 100 | else: 101 | ref = "%s:refs/heads/%s" % (username, branch.getName()) 102 | 103 | token = self.configPersonal.getToken(self.tokenSpace) 104 | master = Branch(self.config.getMaster()) 105 | if isinstance(token, str): 106 | url = "%s/repos/%s/%s/pulls" % ( 107 | self.baseUrl, 108 | username, 109 | self.remote.getRepositoryName() 110 | ) 111 | 112 | data = { 113 | "state": 'open', 114 | "head": ref, 115 | "base": master.getName() 116 | } 117 | headers = {'Authorization': 'token %s' % token} 118 | response = requests.get( 119 | url, 120 | headers=headers, 121 | params=data 122 | ) 123 | 124 | if response.status_code != 200: 125 | raise GitcdGithubApiException( 126 | "Could not fetch open pull requests," + 127 | " please have a look manually." 128 | ) 129 | 130 | result = response.json() 131 | returnValue = {} 132 | if len(result) == 1: 133 | reviewers = self.isReviewedBy( 134 | '%s/%s' % (result[0]['url'], 'reviews') 135 | ) 136 | 137 | returnValue['state'] = 'REVIEW REQUIRED' 138 | 139 | if len(reviewers) == 0: 140 | reviewers = self.getLgtmComments(result[0]['comments_url']) 141 | 142 | if len(reviewers) > 0: 143 | returnValue['state'] = 'APPROVED' 144 | for reviewer in reviewers: 145 | reviewer = reviewers[reviewer] 146 | if reviewer['state'] != 'APPROVED': 147 | returnValue['state'] = reviewer['state'] 148 | 149 | returnValue['master'] = master.getName() 150 | returnValue['feature'] = branch.getName() 151 | returnValue['reviews'] = reviewers 152 | returnValue['url'] = result[0]['html_url'] 153 | returnValue['number'] = result[0]['number'] 154 | 155 | return returnValue 156 | 157 | def isReviewedBy(self, reviewUrl) -> dict: 158 | token = self.configPersonal.getToken(self.tokenSpace) 159 | reviewers = {} 160 | if isinstance(token, str): 161 | if token is not None: 162 | headers = {'Authorization': 'token %s' % token} 163 | response = requests.get( 164 | reviewUrl, 165 | headers=headers 166 | ) 167 | reviews = response.json() 168 | for review in reviews: 169 | if review['user']['login'] in reviewers: 170 | reviewer = reviewers[review['user']['login']] 171 | else: 172 | reviewer = {} 173 | reviewer['comments'] = [] 174 | 175 | comment = {} 176 | comment['body'] = review['body'] 177 | comment['state'] = review['state'] 178 | reviewer['state'] = review['state'] 179 | reviewer['comments'].append(comment) 180 | 181 | reviewers[review['user']['login']] = reviewer 182 | 183 | return reviewers 184 | 185 | def getLgtmComments(self, commentsUrl): 186 | token = self.configPersonal.getToken(self.tokenSpace) 187 | reviewers = {} 188 | if isinstance(token, str): 189 | headers = {'Authorization': 'token %s' % token} 190 | response = requests.get( 191 | commentsUrl, 192 | headers=headers 193 | ) 194 | comments = response.json() 195 | for comment in comments: 196 | if 'lgtm' in comment['body'].lower(): 197 | 198 | if comment['user']['login'] in reviewers: 199 | reviewer = reviewers[comment['user']['login']] 200 | else: 201 | reviewer = {} 202 | reviewer['comments'] = [] 203 | 204 | reviewer['state'] = 'APPROVED' 205 | reviewerComment = {} 206 | reviewerComment['state'] = 'APPROVED' 207 | reviewerComment['body'] = comment['body'] 208 | reviewer['comments'].append(reviewerComment) 209 | reviewers[comment['user']['login']] = reviewer 210 | 211 | return reviewers 212 | -------------------------------------------------------------------------------- /gitcd/git/server/gitlab.py: -------------------------------------------------------------------------------- 1 | from gitcd.git.server import GitServer 2 | from gitcd.git.branch import Branch 3 | 4 | from gitcd.exceptions import GitcdGithubApiException 5 | 6 | import requests 7 | 8 | 9 | class Gitlab(GitServer): 10 | 11 | tokenSpace = 'gitlab' 12 | baseUrl = 'https://gitlab.com/api/v4' 13 | 14 | def open( 15 | self, 16 | title: str, 17 | body: str, 18 | fromBranch: Branch, 19 | toBranch: Branch 20 | ) -> bool: 21 | token = self.configPersonal.getToken(self.tokenSpace) 22 | if token is not None: 23 | 24 | projectId = '%s%s%s' % ( 25 | self.remote.getUsername(), 26 | '%2F', 27 | self.remote.getRepositoryName() 28 | ) 29 | url = '%s/projects/%s/merge_requests' % ( 30 | self.baseUrl, 31 | projectId 32 | ) 33 | 34 | data = { 35 | 'source_branch': fromBranch.getName(), 36 | 'target_branch': toBranch.getName(), 37 | 'title': title, 38 | 'description': body 39 | } 40 | headers = {'Private-Token': token} 41 | response = requests.post( 42 | url, 43 | headers=headers, 44 | json=data 45 | ) 46 | 47 | if response.status_code == 401: 48 | raise GitcdGithubApiException( 49 | "Authentication failed, create a new app password." 50 | ) 51 | 52 | if response.status_code == 409: 53 | raise GitcdGithubApiException( 54 | "This pull-requests already exists." 55 | ) 56 | 57 | # anything else but success 58 | if response.status_code != 201: 59 | raise GitcdGithubApiException( 60 | "Open a pull request on gitlab failed." 61 | ) 62 | 63 | try: 64 | result = response.json() 65 | defaultBrowser = self.getDefaultBrowserCommand() 66 | self.cli.execute("%s %s" % ( 67 | defaultBrowser, 68 | result['web_url'] 69 | )) 70 | except ValueError: 71 | raise GitcdGithubApiException( 72 | "Open a pull request on gitlab failed." 73 | ) 74 | else: 75 | defaultBrowser = self.getDefaultBrowserCommand() 76 | self.cli.execute("%s %s" % ( 77 | defaultBrowser, 78 | "%s/%s/%s/merge_requests/new?%s=%s" % ( 79 | "https://gitlab.com", 80 | self.remote.getUsername(), 81 | self.remote.getRepositoryName(), 82 | 'merge_request%5Bsource_branch%5D', 83 | fromBranch.getName() 84 | ) 85 | )) 86 | return True 87 | 88 | def status(self, branch: Branch): 89 | master = Branch(self.config.getMaster()) 90 | token = self.configPersonal.getToken(self.tokenSpace) 91 | if token is not None: 92 | 93 | data = { 94 | 'state': 'biber', 95 | 'source_branch': branch.getName(), 96 | 'target_branch': master.getName() 97 | } 98 | 99 | projectId = '%s%s%s' % ( 100 | self.remote.getUsername(), 101 | '%2F', 102 | self.remote.getRepositoryName() 103 | ) 104 | baseUrl = "%s/projects/%s/merge_requests" % ( 105 | self.baseUrl, 106 | projectId 107 | ) 108 | url = "%s?state=opened" % ( 109 | baseUrl 110 | ) 111 | headers = {'Private-Token': token} 112 | response = requests.get( 113 | url, 114 | headers=headers, 115 | json=data 116 | ) 117 | 118 | if response.status_code != 200: 119 | raise GitcdGithubApiException( 120 | "Could not fetch open pull requests," + 121 | " please have a look manually." 122 | ) 123 | 124 | returnValue = {} 125 | result = response.json() 126 | if len(result) > 0: 127 | returnValue['state'] = 'REVIEW REQUIRED' 128 | reviewers = self.isReviewedBy( 129 | "%s/%s/approvals" % ( 130 | baseUrl, 131 | result[0]['iid'] 132 | ) 133 | ) 134 | 135 | if len(reviewers) == 0 and result[0]['user_notes_count'] > 0: 136 | reviewers = self.getLgtmComments( 137 | "%s/%s/notes" % ( 138 | baseUrl, 139 | result[0]['iid'] 140 | ) 141 | ) 142 | 143 | if len(reviewers) > 0: 144 | returnValue['state'] = 'APPROVED' 145 | for reviewer in reviewers: 146 | reviewer = reviewers[reviewer] 147 | if reviewer['state'] != 'APPROVED': 148 | returnValue['state'] = reviewer['state'] 149 | 150 | returnValue['master'] = master.getName() 151 | returnValue['feature'] = branch.getName() 152 | returnValue['reviews'] = reviewers 153 | returnValue['url'] = result[0]['web_url'] 154 | returnValue['number'] = result[0]['iid'] 155 | 156 | return returnValue 157 | 158 | def isReviewedBy(self, activityUrl: str) -> dict: 159 | reviewers = {} 160 | token = self.configPersonal.getToken(self.tokenSpace) 161 | if token is not None: 162 | headers = {'Private-Token': token} 163 | response = requests.get( 164 | activityUrl, 165 | headers=headers 166 | ) 167 | if response.status_code != 200: 168 | raise GitcdGithubApiException( 169 | "Fetch PR activity for gitlab failed." 170 | ) 171 | 172 | result = response.json() 173 | if 'approved_by' in result and len(result['approved_by']) > 0: 174 | for approver in result['approved_by']: 175 | reviewer = {} 176 | reviewer['comments'] = [] 177 | comment = {} 178 | comment['body'] = 'approved' 179 | comment['state'] = 'APPROVED' 180 | reviewer['state'] = 'APPROVED' 181 | reviewer['comments'].append(comment) 182 | 183 | reviewers[approver['user']['username']] = reviewer 184 | 185 | return reviewers 186 | 187 | def getLgtmComments(self, commentsUrl): 188 | token = self.configPersonal.getToken(self.tokenSpace) 189 | reviewers = {} 190 | if token is not None: 191 | headers = {'Private-Token': token} 192 | response = requests.get( 193 | commentsUrl, 194 | headers=headers 195 | ) 196 | if response.status_code != 200: 197 | raise GitcdGithubApiException( 198 | "Fetch PR comments for bitbucket failed." 199 | ) 200 | 201 | comments = response.json() 202 | if len(comments) > 0: 203 | for comment in comments: 204 | if ( 205 | 'body' in comment and 206 | 'lgtm' in comment['body'].lower() 207 | ): 208 | if comment['author']['username'] in reviewers: 209 | reviewer = reviewers[comment['author']['username']] 210 | else: 211 | reviewer = {} 212 | reviewer['comments'] = [] 213 | 214 | reviewer['state'] = 'APPROVED' 215 | reviewerComment = {} 216 | reviewerComment['state'] = 'APPROVED' 217 | reviewerComment['body'] = comment['body'] 218 | reviewer['comments'].append(reviewerComment) 219 | reviewers[comment['author']['username']] = reviewer 220 | 221 | return reviewers 222 | -------------------------------------------------------------------------------- /gitcd/git/tag.py: -------------------------------------------------------------------------------- 1 | from gitcd.git import Git 2 | 3 | 4 | class Tag(Git): 5 | 6 | name = 'v' 7 | 8 | def __init__(self, name: str): 9 | self.name = name 10 | 11 | def create(self, message: str) -> bool: 12 | self.verboseCli.execute( 13 | 'git tag -a -m "%s" %s' % ( 14 | message, self.name 15 | ) 16 | ) 17 | return True 18 | 19 | def getName(self) -> str: 20 | return self.name 21 | 22 | def delete(self) -> bool: 23 | output = self.verboseCli.execute("git tag -d %s" % (self.name)) 24 | if output is False: 25 | return False 26 | return True 27 | -------------------------------------------------------------------------------- /gitcd/interface/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gitcd-io/gitcd/303ca1898eaeb7bdd704946cf2466d53b58ea0a0/gitcd/interface/__init__.py -------------------------------------------------------------------------------- /gitcd/interface/cli/__init__.py: -------------------------------------------------------------------------------- 1 | import sys 2 | 3 | import simpcli 4 | 5 | from gitcd.interface.cli.abstract import BaseCommand 6 | from gitcd.interface.cli.clean import Clean 7 | from gitcd.interface.cli.compare import Compare 8 | from gitcd.interface.cli.finish import Finish 9 | from gitcd.interface.cli.init import Init 10 | from gitcd.interface.cli.release import Release 11 | from gitcd.interface.cli.review import Review 12 | from gitcd.interface.cli.start import Start 13 | from gitcd.interface.cli.status import Status 14 | from gitcd.interface.cli.test import Test 15 | from gitcd.interface.cli.upgrade import Upgrade 16 | from gitcd.interface.cli.refresh import Refresh 17 | from gitcd.interface.cli.version import Version 18 | 19 | from gitcd.config import MoveGitcdPersonalPerRepo 20 | 21 | from gitcd.exceptions import GitcdException 22 | from simpcli import CliException 23 | 24 | 25 | class Cli(): 26 | 27 | interface = simpcli.Interface() 28 | 29 | commands = [ 30 | 'init', 31 | 'clean', 32 | 'start', 33 | 'test', 34 | 'review', 35 | 'finish', 36 | 'release', 37 | 'status', 38 | 'compare', 39 | 'upgrade', 40 | 'refresh', 41 | 'version' 42 | ] 43 | 44 | def getAvailableCommands(self): 45 | return self.commands 46 | 47 | def instantiateCommand(self, command: str) -> BaseCommand: 48 | if command == 'init': 49 | return Init() 50 | if command == 'clean': 51 | return Clean() 52 | if command == 'start': 53 | return Start() 54 | if command == 'test': 55 | return Test() 56 | if command == 'review': 57 | return Review() 58 | if command == 'finish': 59 | return Finish() 60 | if command == 'release': 61 | return Release() 62 | if command == 'status': 63 | return Status() 64 | if command == 'compare': 65 | return Compare() 66 | if command == 'upgrade': 67 | return Upgrade() 68 | if command == 'refresh': 69 | return Refresh() 70 | if command == 'version': 71 | return Version() 72 | # probably best to implement a default command 73 | # for command-not-found error 74 | 75 | def dispatch(self, command: str, branch: str): 76 | # this is kind of temporary and will get removed in a few 77 | # releases. It ensures your access token, now stored in all repos 78 | # will be moved into a .gitcd directory in your home directory 79 | MoveGitcdPersonalPerRepo() 80 | 81 | try: 82 | commandObject = self.instantiateCommand(command) 83 | except GitcdException as e: 84 | self.interface.error( 85 | e 86 | ) 87 | sys.exit(1) 88 | except Exception as e: 89 | self.interface.error( 90 | e 91 | ) 92 | sys.exit(1) 93 | 94 | self.interface.header('git-cd %s' % (command)) 95 | 96 | try: 97 | if branch == '*': 98 | branch = commandObject.getDefaultBranch() 99 | else: 100 | branch = commandObject.getRequestedBranch(branch) 101 | 102 | commandObject.run(branch) 103 | # catch cli execution errors here 104 | except (GitcdException, CliException) as e: 105 | self.interface.error(format(e)) 106 | 107 | def close(self, msg: str): 108 | self.interface.ok(msg) 109 | -------------------------------------------------------------------------------- /gitcd/interface/cli/abstract.py: -------------------------------------------------------------------------------- 1 | import simpcli 2 | import sys 3 | 4 | from gitcd.git.repository import Repository 5 | from gitcd.git.branch import Branch 6 | from gitcd.git.tag import Tag 7 | from gitcd.git.remote import Remote 8 | 9 | from gitcd.config import Gitcd as GitcdConfig 10 | from gitcd.config import GitcdPersonal as GitcdPersonalConfig 11 | 12 | from gitcd.exceptions import GitcdNoFeatureBranchException 13 | 14 | 15 | class BaseCommand(object): 16 | 17 | interface = simpcli.Interface() 18 | config = GitcdConfig() 19 | configPersonal = GitcdPersonalConfig() 20 | updateRemote = False 21 | 22 | def __init__(self): 23 | self.instantiateRepository() 24 | if self.updateRemote is True: 25 | self.repository.update() 26 | 27 | def instantiateRepository(self) -> bool: 28 | self.repository = Repository() 29 | return True 30 | 31 | def run(self, branch: Branch): 32 | pass 33 | 34 | def getDefaultBranch(self) -> Branch: 35 | return self.repository.getCurrentBranch() 36 | 37 | def getRequestedBranch(self, branch: str) -> Branch: 38 | featureAsString = self.config.getString(self.config.getFeature()) 39 | if ( 40 | not branch.startswith(featureAsString) and 41 | branch != self.config.getMaster() 42 | ): 43 | branch = '%s%s' % (featureAsString, branch) 44 | return Branch(branch) 45 | 46 | def hasMultipleRemotes(self) -> bool: 47 | return len(self.repository.getRemotes()) > 1 48 | 49 | def getRemote(self, whatFor: str = "") -> str: 50 | remotes = self.repository.getRemotes() 51 | 52 | if len(remotes) == 1: 53 | remote = remotes[0] 54 | else: 55 | if len(remotes) == 0: 56 | default = False 57 | choice = False 58 | else: 59 | default = remotes[0].getName() 60 | choice = [] 61 | for remoteObj in remotes: 62 | choice.append(remoteObj.getName()) 63 | 64 | if whatFor: 65 | whatFor = " {}".format(whatFor) 66 | 67 | msg = "Which remote you want to use{}?".format(whatFor) 68 | remoteAnswer = self.interface.askFor( 69 | msg, 70 | choice, 71 | default 72 | ) 73 | for remoteObj in remotes: 74 | if remoteAnswer == remoteObj.getName(): 75 | remote = remoteObj 76 | 77 | return remote 78 | 79 | def checkTag(self, remote: Remote, tag: Tag) -> bool: 80 | if self.repository.hasUncommitedChanges(): 81 | abort = self.interface.askFor( 82 | "You currently have uncomitted changes." + 83 | " Do you want me to abort and let you commit first?", 84 | ["yes", "no"], 85 | "yes" 86 | ) 87 | 88 | if abort == "yes": 89 | sys.exit(1) 90 | 91 | return True 92 | 93 | def checkRepository(self) -> bool: 94 | # check if repo has uncommited changes 95 | if self.repository.hasUncommitedChanges(): 96 | abort = self.interface.askFor( 97 | "You currently have uncomitted changes." + 98 | " Do you want me to abort and let you commit first?", 99 | ["yes", "no"], 100 | "yes" 101 | ) 102 | 103 | if abort == "yes": 104 | sys.exit(1) 105 | 106 | return True 107 | 108 | def checkBranch(self, remote: Remote, branch: Branch) -> bool: 109 | # check if its a feature branch 110 | if not branch.isFeature(): 111 | raise GitcdNoFeatureBranchException( 112 | "Your current branch is not a valid feature branch." + 113 | " Checkout a feature branch or pass one as param." 114 | ) 115 | 116 | # check remote existence 117 | if not remote.hasBranch(branch): 118 | pushFeatureBranch = self.interface.askFor( 119 | "Your feature branch does not exist on remote." + 120 | " Do you want me to push it remote?", ["yes", "no"], "yes" 121 | ) 122 | 123 | if pushFeatureBranch == "yes": 124 | remote.push(branch) 125 | 126 | # check behind origin 127 | if remote.isBehind(branch): 128 | 129 | pushFeatureBranch = self.interface.askFor( 130 | "Your feature branch is ahead the origin/branch." + 131 | " Do you want me to push the changes?", 132 | ["yes", "no"], 133 | "yes" 134 | ) 135 | 136 | if pushFeatureBranch == "yes": 137 | remote.push(branch) 138 | 139 | return True 140 | 141 | def getTokenOrAskFor(self, tokenSpace: str) -> str: 142 | token = self.configPersonal.getToken(tokenSpace) 143 | if token is None: 144 | token = self.interface.askFor( 145 | "Your personal %s token?" % (tokenSpace), 146 | False, 147 | token 148 | ) 149 | 150 | if ( 151 | tokenSpace == 'bitbucket' and 152 | ':' not in token 153 | ): 154 | self.interface.warning( 155 | 'For bitbucket you need to pass a username' + 156 | ' as well like ' 157 | ) 158 | return self.getTokenOrAskFor(tokenSpace) 159 | 160 | self.configPersonal.setToken(tokenSpace, token) 161 | self.configPersonal.write() 162 | return token 163 | 164 | def mergeWithRetry(self, remote, sourceBranch, targetBranch): 165 | try: 166 | remote.merge(sourceBranch, targetBranch) 167 | except simpcli.CliException: 168 | tryAgain = self.interface.askFor( 169 | "An error occured during the merge.\ 170 | Do you want to fix it and let me try it again?", 171 | ["yes", "no"], "yes" 172 | ) 173 | 174 | if tryAgain == 'yes': 175 | self.mergeWithRetry(remote, sourceBranch, targetBranch) 176 | -------------------------------------------------------------------------------- /gitcd/interface/cli/clean.py: -------------------------------------------------------------------------------- 1 | from gitcd.interface.cli.abstract import BaseCommand 2 | 3 | from gitcd.git.branch import Branch 4 | 5 | from gitcd.app.clean import Clean as CleanHelper 6 | 7 | 8 | class Clean(BaseCommand): 9 | 10 | updateRemote = True 11 | 12 | def run(self, branch: Branch): 13 | helper = CleanHelper() 14 | 15 | branchesToDelete = helper.getBranchesToDelete() 16 | 17 | self.interface.writeOut('Branches to delete') 18 | 19 | if len(branchesToDelete) == 0: 20 | self.interface.ok(' - no branches to delete') 21 | 22 | for branchToDelete in branchesToDelete: 23 | self.interface.red(" - %s" % branchToDelete.getName()) 24 | 25 | self.interface.writeOut('') 26 | if len(branchesToDelete) == 0: 27 | self.interface.ok('Nice, your local repository is clean already.') 28 | return True 29 | 30 | delete = self.interface.askFor( 31 | 'Do you want me to delete those branches locally?', 32 | ['yes', 'no'], 33 | 'yes' 34 | ) 35 | if delete == 'yes': 36 | helper.deleteBranches(branchesToDelete) 37 | 38 | return True 39 | -------------------------------------------------------------------------------- /gitcd/interface/cli/compare.py: -------------------------------------------------------------------------------- 1 | from gitcd.interface.cli.abstract import BaseCommand 2 | 3 | from gitcd.git.repository import Repository 4 | 5 | from gitcd.git.branch import Branch 6 | from gitcd.git.tag import Tag 7 | 8 | 9 | class Compare(BaseCommand): 10 | 11 | def getDefaultBranch(self) -> [Branch, Tag]: 12 | repository = Repository() 13 | return repository.getLatestTag() 14 | 15 | def getRequestedBranch(self, branch: str) -> [Branch, Tag]: 16 | tagPrefix = self.config.getTag() 17 | if branch.startswith(tagPrefix): 18 | branch = Tag(branch) 19 | else: 20 | branch = Branch(branch) 21 | 22 | return branch 23 | 24 | def run(self, branch: [Branch, Tag]): 25 | remote = self.getRemote() 26 | repository = Repository() 27 | currentBranch = repository.getCurrentBranch() 28 | self.checkRepository() 29 | 30 | remote.compare(currentBranch, branch) 31 | -------------------------------------------------------------------------------- /gitcd/interface/cli/finish.py: -------------------------------------------------------------------------------- 1 | from gitcd.interface.cli.abstract import BaseCommand 2 | from gitcd.git.branch import Branch 3 | 4 | 5 | class Finish(BaseCommand): 6 | 7 | def run(self, branch: Branch): 8 | remote = self.getRemote() 9 | 10 | testBranch = self.config.getTest() 11 | masterBranch = self.config.getMaster() 12 | 13 | if branch.getName() == masterBranch: 14 | # maybe i should use recursion here 15 | # if anyone passes master again, i wouldnt notice 16 | branch = Branch('%s%s' % ( 17 | branch.getName(), 18 | self.interface.askFor( 19 | "You passed your master branch name as feature branch,\ 20 | please give a different name." 21 | ) 22 | )) 23 | 24 | if testBranch: 25 | if branch.getName().startswith(testBranch): 26 | # maybe i should use recursion here 27 | # if anyone passes master again, i wouldnt notice 28 | branch = Branch('%s%s' % ( 29 | branch.getName(), 30 | self.interface.askFor( 31 | "You passed your test branch name as feature branch,\ 32 | please give a different branch." 33 | ) 34 | )) 35 | self.checkRepository() 36 | self.checkBranch(remote, branch) 37 | 38 | master = Branch(masterBranch) 39 | 40 | self.mergeWithRetry(remote, master, branch) 41 | 42 | deleteFeatureBranch = self.interface.askFor( 43 | "Delete your feature branch?", ["yes", "no"], "yes" 44 | ) 45 | 46 | if deleteFeatureBranch == "yes": 47 | # delete feature branch remote and locally 48 | remote.delete(branch) 49 | branch.delete() 50 | -------------------------------------------------------------------------------- /gitcd/interface/cli/init.py: -------------------------------------------------------------------------------- 1 | import os 2 | 3 | from gitcd.interface.cli.abstract import BaseCommand 4 | 5 | from gitcd.git.branch import Branch 6 | 7 | 8 | class Init(BaseCommand): 9 | 10 | def run(self, branch: Branch): 11 | self.config.setMaster( 12 | self.interface.askFor( 13 | "Branch name for production releases?", 14 | False, 15 | self.config.getMaster() 16 | ) 17 | ) 18 | 19 | featureDefault = self.config.getFeature() 20 | if featureDefault is None: 21 | featureDefault = '' 22 | self.config.setFeature( 23 | self.interface.askFor( 24 | "Branch name for feature development?", 25 | False, 26 | featureDefault 27 | ) 28 | ) 29 | 30 | testDefault = self.config.getTest() 31 | if testDefault is None: 32 | testDefault = '' 33 | self.config.setTest( 34 | self.interface.askFor( 35 | "Branch name for test releases?", 36 | False, 37 | testDefault 38 | ) 39 | ) 40 | 41 | tagDefault = self.config.getTag() 42 | if tagDefault is None: 43 | tagDefault = '' 44 | self.config.setTag( 45 | self.interface.askFor( 46 | "Version tag prefix?", 47 | False, 48 | tagDefault 49 | ) 50 | ) 51 | 52 | # ask for version type, manual or date 53 | versionType = self.interface.askFor( 54 | "Version type? You can either set your tag number" + 55 | " manually, read it from a version file or generate it by date.", 56 | ['manual', 'date', 'file'], 57 | self.config.getVersionType() 58 | ) 59 | self.config.setVersionType(versionType) 60 | 61 | # if type is date ask for scheme 62 | if versionType == 'date': 63 | versionScheme = self.interface.askFor( 64 | "Scheme for your date-tag?" + 65 | " Year: %Y / Month: %m / Day: %d /" + 66 | " Hour: %H / Minute: %M / Second: %S", 67 | '%Y.%m.%d%H%M', 68 | self.config.getVersionScheme() 69 | ) 70 | elif versionType == 'file': 71 | versionScheme = self.interface.askFor( 72 | "From what file do you want to load your version?", 73 | False, 74 | self.config.getVersionScheme() 75 | ) 76 | if not os.path.isfile(versionScheme): 77 | self.interface.error( 78 | 'Could not find your version file, ' + 79 | 'stick back to manual tag number!' 80 | ) 81 | versionScheme = None 82 | versionType = 'manual' 83 | else: 84 | # you'll be asked for it while a release 85 | versionScheme = None 86 | 87 | extraReleaseCommandDefault = self.config.getExtraReleaseCommand() 88 | if extraReleaseCommandDefault is None: 89 | extraReleaseCommandDefault = '' 90 | self.config.setExtraReleaseCommand( 91 | self.interface.askFor( 92 | "Do you want to execute some additional " + 93 | "commands after a release?", 94 | False, 95 | extraReleaseCommandDefault 96 | ) 97 | ) 98 | 99 | preReleaseCommandDefault = self.config.getPreReleaseCommand() 100 | if preReleaseCommandDefault is None: 101 | preReleaseCommandDefault = '' 102 | self.config.setPreReleaseCommand( 103 | self.interface.askFor( 104 | "Do you want to execute some additional " + 105 | "commands before a release?", 106 | False, 107 | preReleaseCommandDefault 108 | ) 109 | ) 110 | 111 | # pass version scheme to config 112 | self.config.setVersionScheme(versionScheme) 113 | 114 | self.config.write() 115 | -------------------------------------------------------------------------------- /gitcd/interface/cli/refresh.py: -------------------------------------------------------------------------------- 1 | from gitcd.interface.cli.abstract import BaseCommand 2 | 3 | from gitcd.git.branch import Branch 4 | 5 | 6 | class Refresh(BaseCommand): 7 | 8 | updateRemote = True 9 | 10 | def run(self, branch: Branch): 11 | remote = self.getRemote() 12 | master = Branch(self.config.getMaster()) 13 | 14 | if branch.getName() == master.getName(): 15 | # maybe i should use recursion here 16 | # if anyone passes master again, i wouldnt notice 17 | branch = Branch('%s%s' % ( 18 | branch.getName(), 19 | self.interface.askFor( 20 | "You passed your master branch name as feature branch,\ 21 | please give a different branch." 22 | ) 23 | )) 24 | 25 | self.mergeWithRetry(remote, branch, master) 26 | -------------------------------------------------------------------------------- /gitcd/interface/cli/release.py: -------------------------------------------------------------------------------- 1 | from gitcd.interface.cli.abstract import BaseCommand 2 | from gitcd.git.branch import Branch 3 | 4 | from gitcd.app.release import Release as ReleaseHelper 5 | 6 | 7 | class Release(BaseCommand): 8 | 9 | def run(self, branch: Branch): 10 | remote = self.getRemote() 11 | masterBranch = Branch(self.config.getMaster()) 12 | 13 | releaseHelper = ReleaseHelper() 14 | 15 | releaseHelper.checkout(remote, masterBranch) 16 | 17 | version = releaseHelper.getVersion() 18 | if version is False: 19 | version = self.interface.askFor( 20 | "Whats the current version number you want to release?") 21 | 22 | message = self.interface.askFor( 23 | "What message your new release should have?") 24 | # escape double quotes for shell command 25 | message = message.replace('"', '\\"') 26 | 27 | version = '%s%s' % ( 28 | self.config.getString(self.config.getTag()), 29 | version 30 | ) 31 | 32 | releaseHelper.release(version, message, remote) 33 | 34 | return True 35 | -------------------------------------------------------------------------------- /gitcd/interface/cli/review.py: -------------------------------------------------------------------------------- 1 | from gitcd.interface.cli.abstract import BaseCommand 2 | 3 | from gitcd.git.branch import Branch 4 | 5 | 6 | class Review(BaseCommand): 7 | 8 | def run(self, branch: Branch): 9 | remote = self.getRemote("as target remote") 10 | sourceRemote = None 11 | if self.hasMultipleRemotes() is True: 12 | sourceRemote = self.getRemote("as source remote") 13 | if sourceRemote.getUrl() == remote.getUrl(): 14 | sourceRemote = None 15 | 16 | master = Branch(self.config.getMaster()) 17 | 18 | self.checkRepository() 19 | 20 | if sourceRemote is None: 21 | self.checkBranch(remote, branch) 22 | else: 23 | self.checkBranch(sourceRemote, branch) 24 | 25 | self.interface.warning("Opening pull-request") 26 | 27 | # check if pr is open already 28 | pr = remote.getGitWebIntegration() 29 | self.getTokenOrAskFor(pr.getTokenSpace()) 30 | prInfo = pr.status(branch, sourceRemote) 31 | if prInfo is not None and 'url' in prInfo: 32 | openPr = self.interface.askFor( 33 | 'Pull request is already open. ' + 34 | 'Should i open it in your browser?', 35 | ["yes", "no"], 36 | "yes" 37 | ) 38 | 39 | if openPr == "yes": 40 | pr.openBrowser(prInfo['url']) 41 | 42 | return True 43 | 44 | # ask for title and body 45 | title = self.interface.askFor( 46 | 'Pull-Request title?', 47 | False, 48 | branch.getName() 49 | ) 50 | 51 | body = self.interface.askFor("Pull-Request body?") 52 | # go on opening pr 53 | pr.open(title, body, branch, master, sourceRemote) 54 | -------------------------------------------------------------------------------- /gitcd/interface/cli/start.py: -------------------------------------------------------------------------------- 1 | from gitcd.interface.cli.abstract import BaseCommand 2 | from gitcd.git.branch import Branch 3 | 4 | 5 | class Start(BaseCommand): 6 | 7 | def getDefaultBranch(self) -> Branch: 8 | featurePrefix = self.config.getFeature() 9 | featurePrefixAsString = self.config.getString(featurePrefix) 10 | branch = self.interface.askFor( 11 | "Name for your new feature-branch? (without %s prefix)" 12 | % (featurePrefixAsString) 13 | ) 14 | 15 | return self.instantiateBranch(branch.replace(' ', '-')) 16 | 17 | def instantiateBranch(self, branch: str) -> Branch: 18 | featurePrefix = self.config.getFeature() 19 | featurePrefixAsString = self.config.getString(featurePrefix) 20 | 21 | return Branch('%s%s' % (featurePrefixAsString, branch)) 22 | 23 | def checkDoubleFeaturePrefix(self, branch: Branch) -> Branch: 24 | featurePrefix = self.config.getFeature() 25 | featurePrefixAsString = self.config.getString(featurePrefix) 26 | 27 | if not featurePrefix: 28 | return branch 29 | 30 | if branch.getName().startswith('%s%s' % ( 31 | featurePrefixAsString, 32 | featurePrefixAsString 33 | )): 34 | fixFeatureBranch = self.interface.askFor( 35 | "Your feature branch already starts" + 36 | " with your feature prefix," + 37 | " should i remove it for you?", 38 | ["yes", "no"], 39 | "yes" 40 | ) 41 | 42 | if fixFeatureBranch == "yes": 43 | branch = self.instantiateBranch( 44 | branch.getName().replace('%s%s' % ( 45 | featurePrefixAsString, 46 | featurePrefixAsString 47 | ), '') 48 | ) 49 | 50 | return branch 51 | 52 | def run(self, branch: Branch): 53 | remote = self.getRemote() 54 | masterBranch = self.config.getMaster() 55 | featurePrefix = self.config.getFeature() 56 | featurePrefixAsString = self.config.getString(featurePrefix) 57 | testBranch = self.config.getTest() 58 | testBranchAsString = self.config.getString(testBranch) 59 | 60 | # few checks on the new feature branch 61 | if '%s%s' % (featurePrefixAsString, branch.getName()) == masterBranch: 62 | # maybe i should use while here 63 | # if anyone passes master again, i wouldnt notice 64 | branch = self.instantiateBranch(self.interface.askFor( 65 | "You passed your master branch name as feature branch,\ 66 | please give a different name." 67 | )) 68 | 69 | # not sure if this is smart since test branch is kind of a prefix too 70 | if testBranch is not None: 71 | featureBranchString = '%s%s' % ( 72 | featurePrefixAsString, 73 | branch.getName() 74 | ) 75 | if featureBranchString.startswith(testBranchAsString): 76 | # maybe i should use while here 77 | # if anyone passes develop again, i wouldnt notice 78 | branch = self.instantiateBranch(self.interface.askFor( 79 | "You passed your test branch name as feature branch,\ 80 | please give a different name." 81 | )) 82 | 83 | branch = self.checkDoubleFeaturePrefix(branch) 84 | 85 | remote.createFeature(branch) 86 | -------------------------------------------------------------------------------- /gitcd/interface/cli/status.py: -------------------------------------------------------------------------------- 1 | from gitcd.interface.cli.abstract import BaseCommand 2 | 3 | from gitcd.git.branch import Branch 4 | 5 | 6 | class Status(BaseCommand): 7 | 8 | def run(self, branch: str): 9 | remote = self.getRemote("as target remote") 10 | sourceRemote = None 11 | if self.hasMultipleRemotes() is True: 12 | sourceRemote = self.getRemote("as source remote") 13 | if sourceRemote.getUrl() == remote.getUrl(): 14 | sourceRemote = None 15 | 16 | master = Branch(self.config.getMaster()) 17 | pr = remote.getGitWebIntegration() 18 | self.getTokenOrAskFor(pr.getTokenSpace()) 19 | prInfo = pr.status(branch, sourceRemote) 20 | if len(prInfo) == 0: 21 | self.interface.writeOut( 22 | "No pull request exists for %s...%s\n" % ( 23 | branch.getName(), 24 | master.getName() 25 | ) 26 | ) 27 | 28 | self.interface.writeOut('Run') 29 | self.interface.warning( 30 | "git-cd review %s" % ( 31 | branch.getName() 32 | ) 33 | ) 34 | self.interface.writeOut('to create a pull request') 35 | else: 36 | self.interface.writeOut("Branches: %s...%s" % ( 37 | branch.getName(), 38 | master.getName()) 39 | ) 40 | if prInfo['state'] == 'APPROVED': 41 | state = '%s%s%s' % ( 42 | self.interface.OK, 43 | prInfo['state'], 44 | self.interface.ENDC 45 | ) 46 | else: 47 | state = '%s%s%s' % ( 48 | self.interface.ERROR, 49 | prInfo['state'], 50 | self.interface.ENDC 51 | ) 52 | self.interface.writeOut('State: %s' % (state)) 53 | self.interface.writeOut("Number: %s" % (prInfo['number'])) 54 | self.interface.writeOut("Reviews:") 55 | for user in prInfo['reviews']: 56 | review = prInfo['reviews'][user] 57 | self.interface.writeOut(' - %s' % (user)) 58 | for comment in review['comments']: 59 | if comment['state'] == 'APPROVED': 60 | state = '%s%s%s' % ( 61 | self.interface.OK, 62 | comment['state'], 63 | self.interface.ENDC 64 | ) 65 | else: 66 | state = '%s%s%s' % ( 67 | self.interface.ERROR, 68 | comment['state'], 69 | self.interface.ENDC 70 | ) 71 | self.interface.writeOut(' %s: %s' % ( 72 | state, 73 | comment['body'] 74 | )) 75 | 76 | self.interface.writeOut("URL: %s" % (prInfo['url'])) 77 | -------------------------------------------------------------------------------- /gitcd/interface/cli/test.py: -------------------------------------------------------------------------------- 1 | from gitcd.interface.cli.abstract import BaseCommand 2 | from gitcd.git.branch import Branch 3 | from gitcd.git.repository import Repository 4 | 5 | 6 | class Test(BaseCommand): 7 | 8 | updateRemote = True 9 | 10 | def run(self, branch: Branch): 11 | repository = Repository() 12 | remote = self.getRemote() 13 | developmentBranches = repository.getDevelopmentBranches() 14 | if len(developmentBranches) == 1: 15 | developmentBranch = developmentBranches[0] 16 | else: 17 | branchNames = [] 18 | for developmentBranch in developmentBranches: 19 | branchNames.append(developmentBranch.getName()) 20 | 21 | branchNames.reverse() 22 | default = branchNames[0] 23 | choice = branchNames 24 | 25 | developmentBranch = Branch(self.interface.askFor( 26 | "Which develop branch you want to use?", 27 | choice, 28 | default 29 | )) 30 | 31 | self.mergeWithRetry(remote, developmentBranch, branch) 32 | repository.checkoutBranch(branch) 33 | -------------------------------------------------------------------------------- /gitcd/interface/cli/upgrade.py: -------------------------------------------------------------------------------- 1 | from gitcd.interface.cli.abstract import BaseCommand 2 | 3 | from gitcd.app.upgrade import Upgrade as UpgradeHelper 4 | 5 | from gitcd.git.nullRepository import NullRepository 6 | from gitcd.git.branch import Branch 7 | 8 | from gitcd.exceptions import GitcdPyPiApiException 9 | 10 | 11 | class Upgrade(BaseCommand): 12 | 13 | def run(self, branch: Branch): 14 | helper = UpgradeHelper() 15 | 16 | localVersion = helper.getLocalVersion() 17 | 18 | try: 19 | pypiVersion = helper.getPypiVersion() 20 | except GitcdPyPiApiException as e: 21 | pypiVersion = 'unknown' 22 | message = str(e) 23 | 24 | self.interface.info('Local %s' % localVersion) 25 | self.interface.info('PyPi %s' % pypiVersion) 26 | 27 | if pypiVersion == 'unknown': 28 | self.interface.error(message) 29 | return False 30 | 31 | if helper.isUpgradable(): 32 | upgrade = self.interface.askFor( 33 | "Do you want me to upgrade gitcd for you?", 34 | ["yes", "no"], 35 | "yes" 36 | ) 37 | if upgrade == 'yes': 38 | try: 39 | helper.upgrade() 40 | return True 41 | except SystemExit: 42 | self.interface.error('An error occured during the update!') 43 | pass 44 | 45 | self.interface.info( 46 | 'Please upgrade by running pip3 install --user --upgrade gitcd' 47 | ) 48 | return False 49 | else: 50 | self.interface.ok( 51 | 'You seem to be on the most recent version, congratulation!' 52 | ) 53 | return True 54 | 55 | # dont need a real repository for upgrading 56 | def instantiateRepository(self) -> bool: 57 | self.repository = NullRepository() 58 | return True 59 | -------------------------------------------------------------------------------- /gitcd/interface/cli/version.py: -------------------------------------------------------------------------------- 1 | from gitcd.interface.cli.abstract import BaseCommand 2 | 3 | from gitcd.git.branch import Branch 4 | from gitcd.app.upgrade import Upgrade as UpgradeHelper 5 | 6 | 7 | class Version(BaseCommand): 8 | 9 | updateRemote = True 10 | 11 | def run(self, branch: Branch): 12 | helper = UpgradeHelper() 13 | 14 | localVersion = helper.getLocalVersion() 15 | 16 | self.interface.info( 17 | 'You run git-cd in version %s' % (localVersion) 18 | ) 19 | -------------------------------------------------------------------------------- /gitcd/interface/kivy/__init__.py: -------------------------------------------------------------------------------- 1 | # flake8: noqa 2 | 3 | import os 4 | import simpcli 5 | 6 | import kivy 7 | from kivy.lang import Builder 8 | from kivy.app import App 9 | from kivy.properties import StringProperty 10 | from kivy.clock import Clock 11 | from kivymd.theming import ThemeManager 12 | 13 | from gitcd.git.exceptions import NoRepositoryException 14 | 15 | import inspect 16 | 17 | 18 | class Kivy(App): 19 | currentDirectory = StringProperty() 20 | cli = simpcli.Command() 21 | 22 | theme_cls = ThemeManager() 23 | theme_cls.theme_style = 'Dark' 24 | theme_cls.primary_palette = 'LightGreen' 25 | theme_cls.accent_palette = 'Orange' 26 | 27 | def setCurrentDirectory(self, directory: str): 28 | if not os.path.exists(directory): 29 | # error dialog maybe? 30 | return False 31 | 32 | self.currentDirectory = directory 33 | os.chdir(self.currentDirectory) 34 | 35 | def getCurrentRepository(self): 36 | return self.currentRepository 37 | 38 | def build(self): 39 | 40 | return Builder.load_string(""" 41 | #:import NavigationLayout kivymd.navigationdrawer.NavigationLayout 42 | #:import MDThemePicker kivymd.theme_picker.MDThemePicker 43 | #:import GitcdUpgradeDialog gitcd.interface.kivy.upgrade.GitcdUpgradeDialog 44 | #:import GitcdCleanDialog gitcd.interface.kivy.clean.GitcdCleanDialog 45 | #:import GitcdNavigationDrawer gitcd.interface.kivy.navigation.GitcdNavigationDrawer 46 | #:import GitcdBranchPanel gitcd.interface.kivy.branchpanel.GitcdBranchPanel 47 | #:import GitcdTagPanel gitcd.interface.kivy.tagpanel.GitcdTagPanel 48 | #:import GitcdInfoPanel gitcd.interface.kivy.infopanel.GitcdInfoPanel 49 | #:import GitcdMainPanel gitcd.interface.kivy.mainpanel.GitcdMainPanel 50 | 51 | 52 | NavigationLayout: 53 | id: nav_layout 54 | side_panel_width: dp(500) 55 | GitcdNavigationDrawer: 56 | id: nav_drawer 57 | BoxLayout: 58 | orientation: 'vertical' 59 | Toolbar: 60 | id: toolbar 61 | title: "You are currently in " + app.currentDirectory 62 | md_bg_color: app.theme_cls.primary_color 63 | background_palette: 'Primary' 64 | background_hue: '500' 65 | left_action_items: [['folder-outline', lambda x: app.root.toggle_nav_drawer()]] 66 | right_action_items: [['sync', lambda x: GitcdCleanDialog().open()], ['help', lambda x: GitcdUpgradeDialog().open()], ['format-color-fill', lambda x: MDThemePicker().open()]] 67 | BoxLayout: 68 | spacing: 20 69 | padding: [20, 20, 20, 20] 70 | GitcdMainPanel: 71 | id: main_panel 72 | # GitcdBranchPanel: 73 | # id: branch_panel 74 | # MDSeparator: 75 | # width: dp(1) 76 | # GitcdTagPanel: 77 | # id: tag_panel 78 | # MDSeparator: 79 | # width: dp(1) 80 | # GitcdInfoPanel: 81 | # id: info_panel 82 | 83 | 84 | """) 85 | -------------------------------------------------------------------------------- /gitcd/interface/kivy/branchpanel.py: -------------------------------------------------------------------------------- 1 | # flake8: noqa 2 | import threading 3 | 4 | import kivy 5 | from kivy.lang import Builder 6 | 7 | from gitcd.interface.kivy.panel import GitcdInlineNavigationPanel 8 | 9 | 10 | Builder.load_string(''' 11 | #:import MDSpinner kivymd.spinner.MDSpinner 12 | #:import MDNavigationDrawer kivymd.navigationdrawer.MDNavigationDrawer 13 | #:import NavigationDrawerIconButton kivymd.navigationdrawer.NavigationDrawerIconButton 14 | 15 | : 16 | do_scroll_x: False 17 | id: branch_panel 18 | MDNavigationDrawer: 19 | id: branch_list 20 | NavigationDrawerIconButton: 21 | text: "test-branch-1" 22 | icon: 'source-branch' 23 | on_release: root.onRelease 24 | NavigationDrawerIconButton: 25 | text: "test-branch-2" 26 | icon: 'source-branch' 27 | on_release: root.onRelease 28 | NavigationDrawerIconButton: 29 | text: "test-branch-3" 30 | icon: 'source-branch' 31 | on_release: root.onRelease 32 | NavigationDrawerIconButton: 33 | text: "test-branch-4" 34 | icon: 'source-branch' 35 | on_release: root.onRelease 36 | NavigationDrawerIconButton: 37 | text: "test-branch-5" 38 | icon: 'source-branch' 39 | on_release: root.onRelease 40 | NavigationDrawerIconButton: 41 | text: "test-branch-6" 42 | icon: 'source-branch' 43 | on_release: root.onRelease 44 | NavigationDrawerIconButton: 45 | text: "test-branch-7" 46 | icon: 'source-branch' 47 | on_release: root.onRelease 48 | NavigationDrawerIconButton: 49 | text: "test-branch-8" 50 | icon: 'source-branch' 51 | on_release: root.onRelease 52 | NavigationDrawerIconButton: 53 | text: "test-branch-9" 54 | icon: 'source-branch' 55 | on_release: root.onRelease 56 | NavigationDrawerIconButton: 57 | text: "test-branch-10" 58 | icon: 'source-branch' 59 | on_release: root.onRelease 60 | NavigationDrawerIconButton: 61 | text: "test-branch-11" 62 | icon: 'source-branch' 63 | on_release: root.onRelease 64 | NavigationDrawerIconButton: 65 | text: "test-branch-12" 66 | icon: 'source-branch' 67 | on_release: root.onRelease 68 | NavigationDrawerIconButton: 69 | text: "test-branch-13" 70 | icon: 'source-branch' 71 | on_release: root.onRelease 72 | NavigationDrawerIconButton: 73 | text: "test-branch-14" 74 | icon: 'source-branch' 75 | on_release: root.onRelease 76 | 77 | ''') 78 | 79 | 80 | class GitcdBranchPanel(GitcdInlineNavigationPanel): 81 | 82 | def onRelease(self, **kwargs): 83 | pass 84 | -------------------------------------------------------------------------------- /gitcd/interface/kivy/clean.py: -------------------------------------------------------------------------------- 1 | # flake8: noqa 2 | import threading 3 | 4 | import kivy 5 | from kivy.lang import Builder 6 | 7 | from kivy.uix.floatlayout import FloatLayout 8 | from kivy.uix.modalview import ModalView 9 | 10 | from kivymd.list import ILeftBody, ILeftBodyTouch, IRightBodyTouch, BaseListItem, OneLineIconListItem 11 | from kivymd.button import MDIconButton 12 | 13 | from gitcd.app.clean import Clean as CleanHelper 14 | 15 | import time 16 | 17 | 18 | Builder.load_string(''' 19 | #:import MDSpinner kivymd.spinner.MDSpinner 20 | 21 | : 22 | size_hint: (None, None) 23 | size: dp(384), dp(80)+dp(290) 24 | 25 | canvas: 26 | Color: 27 | rgb: app.theme_cls.primary_color 28 | Rectangle: 29 | size: self.width, dp(80) 30 | pos: root.pos[0], root.pos[1] + root.height-dp(80) 31 | Color: 32 | rgb: app.theme_cls.bg_normal 33 | Rectangle: 34 | size: self.width, dp(290) 35 | pos: root.pos[0], root.pos[1] + root.height-(dp(80)+dp(290)) 36 | 37 | MDLabel: 38 | font_style: "Headline" 39 | text: "Clean" 40 | size_hint: (None, None) 41 | size: dp(80), dp(50) 42 | pos_hint: {'center_x': 0.5, 'center_y': 0.9} 43 | theme_text_color: 'Custom' 44 | 45 | MDSpinner: 46 | id: spinner 47 | size_hint: None, None 48 | size: dp(46), dp(46) 49 | pos_hint: {'center_x': 0.5, 'center_y': 0.5} 50 | active: True 51 | 52 | ScrollView: 53 | do_scroll_x: False 54 | pos_hint: {'center_x': 0.5, 'center_y': 0.45} 55 | size_hint: (None, None) 56 | size: dp(344), dp(200) 57 | 58 | MDList: 59 | id: list 60 | 61 | MDRaisedButton: 62 | id: buttonClean 63 | pos: root.pos[0] + dp(10), root.pos[1] + dp(10) 64 | text: "Clean" 65 | on_release: root.clean() 66 | disabled: True 67 | MDRaisedButton: 68 | pos: root.pos[0]+root.size[0]-self.width-dp(10), root.pos[1] + dp(10) 69 | text: "Close" 70 | on_release: root.dismiss() 71 | ''') 72 | 73 | 74 | class IconLeftSampleWidget(ILeftBodyTouch, MDIconButton): 75 | pass 76 | 77 | 78 | class GitcdCleanDialog(FloatLayout, ModalView): 79 | 80 | app = None 81 | branches = [] 82 | tags = [] 83 | helper = CleanHelper() 84 | 85 | def open(self, **kwargs): 86 | super(GitcdCleanDialog, self).open(**kwargs) 87 | self.app = kivy.app.App.get_running_app() 88 | self.branches = [] 89 | self.tags = [] 90 | 91 | threading.Thread(target=self.loadBranches).start() 92 | 93 | def loadBranches(self): 94 | self.branches = self.helper.getBranchesToDelete() 95 | 96 | self.remove_widget(self.ids.spinner) 97 | 98 | for branch in self.branches: 99 | item = OneLineIconListItem( 100 | text=branch.getName(), 101 | disabled=True 102 | ) 103 | item.add_widget(IconLeftSampleWidget( 104 | icon='source-branch' 105 | )) 106 | self.ids.list.add_widget(item) 107 | time.sleep(0.2) 108 | 109 | if len(self.branches) > 0: 110 | self.ids.buttonClean.disabled = False 111 | 112 | def clean(self): 113 | self.helper.deleteBranches(self.branches) 114 | self.dismiss() 115 | -------------------------------------------------------------------------------- /gitcd/interface/kivy/infopanel.py: -------------------------------------------------------------------------------- 1 | # flake8: noqa 2 | import threading 3 | 4 | import kivy 5 | from kivy.lang import Builder 6 | 7 | from kivy.uix.scrollview import ScrollView 8 | 9 | from kivymd.list import ILeftBodyTouch, OneLineIconListItem 10 | from kivymd.button import MDIconButton 11 | from kivymd.card import MDCard 12 | 13 | 14 | Builder.load_string(''' 15 | #:import MDSpinner kivymd.spinner.MDSpinner 16 | 17 | : 18 | size_hint: None, None 19 | size: dp(320), dp(180) 20 | pos_hint: {'center_x': 0, 'center_y': 1} 21 | BoxLayout: 22 | orientation:'vertical' 23 | padding: dp(8) 24 | MDLabel: 25 | text: 'Title' 26 | theme_text_color: 'Secondary' 27 | font_style:"Title" 28 | size_hint_y: None 29 | height: dp(36) 30 | MDSeparator: 31 | height: dp(1) 32 | MDLabel: 33 | text: 'Body' 34 | theme_text_color: 'Primary' 35 | ''') 36 | 37 | 38 | class IconLeftSampleWidget(ILeftBodyTouch, MDIconButton): 39 | pass 40 | 41 | 42 | class GitcdInfoPanel(MDCard): 43 | 44 | app = None 45 | branches = [] 46 | tags = [] 47 | 48 | def __init__(self, **kwargs): 49 | super(GitcdInfoPanel, self).__init__(**kwargs) 50 | threading.Thread(target=self.initialize).start() 51 | 52 | def initialize(self, **kwargs): 53 | pass 54 | -------------------------------------------------------------------------------- /gitcd/interface/kivy/mainpanel.py: -------------------------------------------------------------------------------- 1 | # flake8: noqa 2 | import threading 3 | 4 | import kivy 5 | from kivy.lang import Builder 6 | 7 | from kivy.uix.boxlayout import BoxLayout 8 | 9 | from kivymd.list import ILeftBodyTouch, OneLineIconListItem 10 | from kivymd.button import MDIconButton 11 | from kivymd.tabs import MDTabbedPanel 12 | 13 | 14 | Builder.load_string(''' 15 | #:import MDSpinner kivymd.spinner.MDSpinner 16 | #:import MDTab kivymd.tabs.MDTab 17 | #:import GitcdBranchPanel gitcd.interface.kivy.branchpanel.GitcdBranchPanel 18 | #:import GitcdTagPanel gitcd.interface.kivy.tagpanel.GitcdTagPanel 19 | 20 | : 21 | id: main_panel 22 | spacing: 20 23 | MDTabbedPanel: 24 | id:tab_panel 25 | size_hint: (0.4, 1) 26 | tab_display_mode:'text' 27 | MDTab: 28 | name: 'branches' 29 | text: "Branches" # Why are these not set!!! 30 | GitcdBranchPanel: 31 | id: branch_panels 32 | MDTab: 33 | name: 'tags' 34 | text: 'Tags' 35 | GitcdTagPanel: 36 | id: tab_panels 37 | BoxLayout: 38 | orientation: 'vertical' 39 | 40 | Toolbar: 41 | title: "Toolbar with left and right buttons" 42 | pos_hint: {'center_x': 0.5, 'center_y': 0.25} 43 | md_bg_color: app.theme_cls.bg_light 44 | specific_text_color: app.theme_cls.text_color 45 | left_action_items: [['arrow-left', lambda x: None]] 46 | right_action_items: [['lock', lambda x: None], \ 47 | ['camera', lambda x: None], \ 48 | ['play', lambda x: None]] 49 | MDLabel: 50 | color: app.theme_cls.text_color 51 | text: 'Some text, here is the info panel with actions' 52 | ''') 53 | 54 | 55 | class IconLeftSampleWidget(ILeftBodyTouch, MDIconButton): 56 | pass 57 | 58 | 59 | class GitcdMainPanel(BoxLayout): 60 | 61 | app = None 62 | branches = [] 63 | tags = [] 64 | 65 | def __init__(self, **kwargs): 66 | super(GitcdMainPanel, self).__init__(**kwargs) 67 | threading.Thread(target=self.initialize).start() 68 | 69 | def initialize(self, **kwargs): 70 | pass 71 | 72 | 73 | 74 | 75 | # MDTabbedPanel: 76 | # id: tab_panel 77 | # tab_display_mode:'text' 78 | 79 | # MDTab: 80 | # name: 'music' 81 | # text: "Music" # Why are these not set!!! 82 | # icon: "playlist-play" 83 | # MDLabel: 84 | # font_style: 'Body1' 85 | # theme_text_color: 'Primary' 86 | # text: "Here is my music list :)" 87 | # halign: 'center' 88 | # MDTab: 89 | # name: 'movies' 90 | # text: 'Movies' 91 | # icon: "movie" 92 | 93 | # MDLabel: 94 | # font_style: 'Body1' 95 | # theme_text_color: 'Primary' 96 | # text: "Show movies here :)" 97 | # halign: 'center' -------------------------------------------------------------------------------- /gitcd/interface/kivy/navigation.py: -------------------------------------------------------------------------------- 1 | # flake8: noqa 2 | import threading 3 | 4 | import kivy 5 | from kivy.lang import Builder 6 | from kivymd.navigationdrawer import MDNavigationDrawer 7 | from kivymd.navigationdrawer import NavigationDrawerIconButton 8 | 9 | import simpcli 10 | 11 | import time 12 | 13 | Builder.load_string(''' 14 | #:import NavigationDrawerToolbar kivymd.navigationdrawer.NavigationDrawerToolbar 15 | 16 | : 17 | id: nav_drawer 18 | NavigationDrawerToolbar: 19 | id: toolbar 20 | title: "Your repositories" 21 | right_action_items: [['folder-plus', lambda x: app.root.toggle_nav_drawer()]] 22 | left_action_items: [['close', lambda x: app.root.toggle_nav_drawer()]] 23 | MDSpinner: 24 | id: spinner 25 | size_hint: None, None 26 | size: dp(25), dp(25) 27 | pos_hint: {'center_x': 1, 'center_y': 0.5} 28 | active: True 29 | ''') 30 | 31 | 32 | class GitcdNavigationDrawer(MDNavigationDrawer): 33 | 34 | app = None 35 | 36 | def __init__(self, **kwargs): 37 | super(GitcdNavigationDrawer, self).__init__(**kwargs) 38 | threading.Thread(target=self.initialize).start() 39 | 40 | def readGitCdFolders(self): 41 | cli = simpcli.Command() 42 | result = cli.execute('find ~ -path "*/.gitcd" 2>/dev/null', True) 43 | folders = result.split("\n") 44 | gitFolders = [] 45 | for folder in folders: 46 | folder = folder.replace('/.gitcd', '') 47 | folderParts = folder.split('/') 48 | gitFolder = { 49 | 'name': folderParts[-1], 50 | 'path': folder 51 | } 52 | gitFolders.append(gitFolder) 53 | return gitFolders 54 | 55 | def initialize(self): 56 | self.app = kivy.app.App.get_running_app() 57 | 58 | gitFolders = self.readGitCdFolders() 59 | 60 | self.ids.toolbar.remove_widget(self.ids.spinner) 61 | for folder in gitFolders: 62 | button = NavigationDrawerIconButton( 63 | text=folder['name'], 64 | on_release=self.onRelease 65 | ) 66 | button.icon = 'github-circle' 67 | button.path = folder['path'] 68 | self.add_widget(button) 69 | time.sleep(0.2) 70 | 71 | def onRelease(self, button): 72 | self.app.setCurrentDirectory(button.path) 73 | 74 | -------------------------------------------------------------------------------- /gitcd/interface/kivy/panel.py: -------------------------------------------------------------------------------- 1 | # flake8: noqa 2 | import threading 3 | import time 4 | 5 | from kivy.uix.scrollview import ScrollView 6 | 7 | 8 | class GitcdFakeNavigationPanel(object): 9 | def toggle_state(self): 10 | pass 11 | 12 | 13 | class GitcdInlineNavigationPanel(ScrollView): 14 | 15 | def __init__(self, **kwargs): 16 | super(GitcdInlineNavigationPanel, self).__init__(**kwargs) 17 | threading.Thread(target=self.initialize).start() 18 | 19 | def initialize(self, **kwargs): 20 | while len(self.ids) <= 0: 21 | time.sleep(0.001) 22 | for id in self.ids: 23 | self.ids[id].panel = GitcdFakeNavigationPanel() 24 | -------------------------------------------------------------------------------- /gitcd/interface/kivy/tagpanel.py: -------------------------------------------------------------------------------- 1 | # flake8: noqa 2 | import threading 3 | 4 | import kivy 5 | from kivy.lang import Builder 6 | 7 | from gitcd.interface.kivy.panel import GitcdInlineNavigationPanel 8 | 9 | 10 | Builder.load_string(''' 11 | #:import MDSpinner kivymd.spinner.MDSpinner 12 | #:import MDNavigationDrawer kivymd.navigationdrawer.MDNavigationDrawer 13 | #:import NavigationDrawerIconButton kivymd.navigationdrawer.NavigationDrawerIconButton 14 | 15 | : 16 | do_scroll_x: False 17 | id: branch_panel 18 | MDNavigationDrawer: 19 | id: branch_list 20 | NavigationDrawerIconButton: 21 | text: "v0.0.1" 22 | icon: 'tag' 23 | on_release: root.onRelease 24 | NavigationDrawerIconButton: 25 | text: "v0.0.2" 26 | icon: 'tag' 27 | on_release: root.onRelease 28 | NavigationDrawerIconButton: 29 | text: "v0.0.3" 30 | icon: 'tag' 31 | on_release: root.onRelease 32 | NavigationDrawerIconButton: 33 | text: "v0.0.4" 34 | icon: 'tag' 35 | on_release: root.onRelease 36 | NavigationDrawerIconButton: 37 | text: "v0.0.5" 38 | icon: 'tag' 39 | on_release: root.onRelease 40 | NavigationDrawerIconButton: 41 | text: "v0.0.6" 42 | icon: 'tag' 43 | on_release: root.onRelease 44 | NavigationDrawerIconButton: 45 | text: "v0.0.7" 46 | icon: 'tag' 47 | on_release: root.onRelease 48 | NavigationDrawerIconButton: 49 | text: "v0.0.8" 50 | icon: 'tag' 51 | on_release: root.onRelease 52 | NavigationDrawerIconButton: 53 | text: "v0.0.9" 54 | icon: 'tag' 55 | on_release: root.onRelease 56 | NavigationDrawerIconButton: 57 | text: "v0.0.10" 58 | icon: 'tag' 59 | on_release: root.onRelease 60 | NavigationDrawerIconButton: 61 | text: "v0.0.11" 62 | icon: 'tag' 63 | on_release: root.onRelease 64 | NavigationDrawerIconButton: 65 | text: "v0.0.12" 66 | icon: 'tag' 67 | on_release: root.onRelease 68 | NavigationDrawerIconButton: 69 | text: "v0.0.13" 70 | icon: 'tag' 71 | on_release: root.onRelease 72 | NavigationDrawerIconButton: 73 | text: "v0.0.14" 74 | icon: 'tag' 75 | on_release: root.onRelease 76 | 77 | ''') 78 | 79 | 80 | class GitcdTagPanel(GitcdInlineNavigationPanel): 81 | 82 | def onRelease(self, **kwargs): 83 | pass 84 | -------------------------------------------------------------------------------- /gitcd/interface/kivy/upgrade.py: -------------------------------------------------------------------------------- 1 | # flake8: noqa 2 | from packaging import version 3 | import threading 4 | 5 | from kivy.lang import Builder 6 | 7 | from kivy.uix.floatlayout import FloatLayout 8 | from kivy.uix.modalview import ModalView 9 | 10 | from kivymd.label import MDLabel 11 | 12 | from gitcd.app.upgrade import Upgrade 13 | 14 | from gitcd.exceptions import GitcdPyPiApiException 15 | 16 | 17 | Builder.load_string(''' 18 | #:import MDSpinner kivymd.spinner.MDSpinner 19 | 20 | : 21 | size_hint: (None, None) 22 | size: dp(284), dp(80)+dp(290) 23 | 24 | canvas: 25 | Color: 26 | rgb: app.theme_cls.primary_color 27 | Rectangle: 28 | size: self.width, dp(80) 29 | pos: root.pos[0], root.pos[1] + root.height-dp(80) 30 | Color: 31 | rgb: app.theme_cls.bg_normal 32 | Rectangle: 33 | size: self.width, dp(290) 34 | pos: root.pos[0], root.pos[1] + root.height-(dp(80)+dp(290)) 35 | 36 | MDLabel: 37 | font_style: "Headline" 38 | text: "Upgrade" 39 | size_hint: (None, None) 40 | size: dp(100), dp(50) 41 | pos_hint: {'center_x': 0.5, 'center_y': 0.9} 42 | theme_text_color: 'Custom' 43 | 44 | MDSpinner: 45 | id: spinner 46 | size_hint: None, None 47 | size: dp(46), dp(46) 48 | pos_hint: {'center_x': 0.5, 'center_y': 0.5} 49 | active: True 50 | 51 | MDRaisedButton: 52 | id: buttonUpgrade 53 | pos: root.pos[0] + dp(10), root.pos[1] + dp(10) 54 | text: "Upgrade" 55 | on_release: root.upgrade() 56 | disabled: True 57 | MDRaisedButton: 58 | pos: root.pos[0]+root.size[0]-self.width-dp(10), root.pos[1] + dp(10) 59 | text: "Close" 60 | on_release: root.dismiss() 61 | ''') 62 | 63 | class GitcdUpgradeDialog(FloatLayout, ModalView): 64 | 65 | helper = Upgrade() 66 | 67 | def open(self, **kwargs): 68 | super(GitcdUpgradeDialog, self).open(**kwargs) 69 | threading.Thread(target=self.loadVersions).start() 70 | 71 | def loadVersions(self): 72 | localVersion = self.helper.getLocalVersion() 73 | 74 | try: 75 | pypiVersion = self.helper.getPypiVersion() 76 | except GitcdPyPiApiException as e: 77 | pypiVersion = 'error could not fetch the api' 78 | 79 | versionText = 'Local Version: %s' % localVersion 80 | versionText += '\nPyPI Version: %s' % pypiVersion 81 | 82 | label = MDLabel( 83 | text = versionText, 84 | theme_text_color = 'Primary', 85 | size_hint = [None, None], 86 | size = [264, 40], 87 | pos_hint = {'center_x': 0.3, 'center_y': 0.65} 88 | ) 89 | 90 | self.remove_widget(self.ids.spinner) 91 | self.add_widget(label) 92 | 93 | if self.helper.isUpgradable(): 94 | self.ids.buttonUpgrade.disabled = False 95 | 96 | def upgrade(self): 97 | self.helper.upgrade() 98 | -------------------------------------------------------------------------------- /gitcd/package.py: -------------------------------------------------------------------------------- 1 | import pkg_resources 2 | import requests 3 | import pip 4 | from gitcd.exceptions import GitcdPyPiApiException 5 | 6 | 7 | class Package(object): 8 | 9 | packageUrl = 'https://pypi.org/pypi/gitcd/json' 10 | 11 | def upgrade(self): 12 | pip.main(['install', '--user', '--upgrade', 'gitcd']) 13 | 14 | def getLocalVersion(self): 15 | return pkg_resources.get_distribution("gitcd").version 16 | 17 | def getPypiVersion(self): 18 | response = requests.get( 19 | self.packageUrl 20 | ) 21 | 22 | if response.status_code != 200: 23 | raise GitcdPyPiApiException( 24 | "Could not fetch version info on PyPi site." + 25 | "You need to check manually, sorry for that." 26 | ) 27 | 28 | result = response.json() 29 | return result['info']['version'] 30 | -------------------------------------------------------------------------------- /publish.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | rm -rf dist/ build/ *.egg-info/ 4 | python3 setup.py bdist_wheel 5 | twine upload dist/* 6 | -------------------------------------------------------------------------------- /requirements-dev.txt: -------------------------------------------------------------------------------- 1 | flake8==3.8.4 2 | bandit==1.6.2 -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | from setuptools import setup, find_packages 4 | 5 | 6 | def read(fpath): 7 | with open(fpath, 'r') as f: 8 | return f.read() 9 | 10 | 11 | def version(fpath): 12 | return read(fpath).strip() 13 | 14 | 15 | setup( 16 | name='gitcd', 17 | version=version('version.txt'), 18 | description='Tool for continuous delivery using git', 19 | long_description=read('README.rst'), 20 | author='Claudio Walser', 21 | author_email='info@gitcd.io', 22 | url='https://www.gitcd.io', 23 | packages=find_packages(), 24 | install_requires=[ 25 | 'simpcli', 26 | 'pyyaml', 27 | 'argparse', 28 | 'argcomplete', 29 | 'requests', 30 | 'packaging', 31 | 'typing' 32 | ], 33 | 34 | entry_points={ 35 | 'console_scripts': [ 36 | 'git-cd = gitcd.bin.console:main', 37 | 'git-cd-ui = gitcd.bin.kivy:main', 38 | ] 39 | }, 40 | license='Apache License', 41 | keywords=['git', 'application', 'continuos delivery'], 42 | classifiers=[ 43 | 'Development Status :: 5 - Production/Stable', 44 | 'Environment :: Console', 45 | 'Intended Audience :: Developers', 46 | 'License :: OSI Approved :: Apache Software License', 47 | 'Operating System :: OS Independent', 48 | 'Programming Language :: Python :: 3.5', 49 | 'Programming Language :: Python :: 3.6', 50 | 'Programming Language :: Python :: 3.7', 51 | 'Programming Language :: Python :: 3.8', 52 | 'Topic :: Utilities' 53 | ] 54 | ) 55 | -------------------------------------------------------------------------------- /version.txt: -------------------------------------------------------------------------------- 1 | 2.2.3 2 | --------------------------------------------------------------------------------