diff --git a/.appveyor.yml b/.appveyor.yml index aa0f705..1148976 100644 --- a/.appveyor.yml +++ b/.appveyor.yml @@ -1,27 +1,34 @@ # Based on pygit2's appveyor config +image: Visual Studio 2019 environment: matrix: - - GENERATOR: 'Visual Studio 10' - PYTHON: 'C:\Python27\python.exe' - - GENERATOR: 'Visual Studio 10 Win64' - PYTHON: 'C:\Python27-x64\python.exe' + - GENERATOR: 'Visual Studio 14' + PYTHON: 'C:\Python38\python.exe' + - GENERATOR: 'Visual Studio 14 Win64' + PYTHON: 'C:\Python38-x64\python.exe' + - GENERATOR: 'Visual Studio 14' + PYTHON: 'C:\Python39\python.exe' + - GENERATOR: 'Visual Studio 14 Win64' + PYTHON: 'C:\Python39-x64\python.exe' init: - - cmd: '%PYTHON% -m pip install -U nose wheel' + - cmd: '%PYTHON% -m pip install -U pip' + - cmd: '%PYTHON% -m pip install -U nose' -build: off +build_script: + # build and install `gl` binary (end to end test also use it) + - cmd: '%PYTHON% -m pip install -r requirements.txt .' before_test: - cmd: git config --global user.name "appveyor-test" - cmd: git config --global user.email "appveyor@test.com" test_script: + - cmd: dir /a:h - ps: | - # e2e tests require `gl` binary - &$env:PYTHON -m pip install -r requirements.txt . # 'gl' is installed in Python Scripts directory $env:PATH += ";$(Split-Path $env:PYTHON)\Scripts" - &$env:PYTHON setup.py nosetests --logging-level=WARN --with-xunit + &$env:PYTHON setup.py nosetests --logging-level=WARN --with-xunit if ($LastExitCode -ne 0) { $host.SetShouldExit($LastExitCode) } # upload results to AppVeyor $wc = New-Object 'System.Net.WebClient' @@ -30,4 +37,3 @@ test_script: branches: only: - master - - develop diff --git a/.snapcraft/travis_snapcraft.cfg b/.snapcraft/travis_snapcraft.cfg deleted file mode 100644 index dabd4f2..0000000 Binary files a/.snapcraft/travis_snapcraft.cfg and /dev/null differ diff --git a/.travis.sh b/.travis.sh index 185b179..c6bb06e 100755 --- a/.travis.sh +++ b/.travis.sh @@ -1,8 +1,19 @@ #!/bin/sh +# Based on pygit2's .travis.sh +PREFIX=/home/travis/install + +# Build libssh2 1.9.0 (Ubuntu only has 1.8.0, which doesn't work) cd ~ +wget https://www.libssh2.org/download/libssh2-1.9.0.tar.gz +tar xf libssh2-1.9.0.tar.gz +cd libssh2-1.9.0 +./configure --prefix=/usr --disable-static && make +sudo make install -git clone --depth=1 -b maint/v0.26 https://github.com/libgit2/libgit2.git +# Build libgit2 +cd ~ +git clone --depth=1 -b "maint/v1.1" https://github.com/libgit2/libgit2.git cd libgit2/ mkdir build && cd build diff --git a/.travis.yml b/.travis.yml index 62ca0df..2736397 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,13 +1,9 @@ language: python -services: -- docker +dist: focal python: -- '2.7' -- '3.3' -- '3.4' -- '3.5' -- '3.6' -- pypy-5.3.1 + - '3.8' + - '3.9' + env: LIBGIT2=~/libgit2/_install/ LD_LIBRARY_PATH=~/libgit2/_install/lib before_install: ./.travis.sh install: pip install -r requirements.txt . @@ -15,30 +11,28 @@ before_script: - git config --global user.name "travis-test" - git config --global user.email "travis@test.com" script: -- nosetests --logging-level=WARN -- nosetests gitless/tests/test_e2e.py --logging-level=WARN +- python -m unittest discover gitless/tests branches: only: - master - - develop -sudo: required jobs: include: - stage: Pack snap + addons: + snaps: + - name: snapcraft + classic: true + - name: multipass + classic: true + channel: beta env: EMPTY before_install: skip install: skip - script: > - docker run -v $(pwd):$(pwd) -w $(pwd) snapcore/snapcraft:stable sh -c "apt-get - update && snapcraft" - after_success: - - openssl aes-256-cbc -K $encrypted_8b01c2e263fa_key -iv $encrypted_8b01c2e263fa_iv - -in .snapcraft/travis_snapcraft.cfg -out .snapcraft/snapcraft.cfg -d + script: sudo snapcraft --destructive-mode deploy: on: branch: master - provider: script - script: > - docker run -v $(pwd):$(pwd) -w $(pwd) snapcore/snapcraft:stable sh -c "apt-get - update && snapcraft && snapcraft push *.snap --release edge" + provider: snap + snap: "*.snap" + channel: edge skip_cleanup: true diff --git a/README.md b/README.md index 0a82051..e60c92e 100644 --- a/README.md +++ b/README.md @@ -1,34 +1,39 @@ Gitless ======= -[![PyPI version](https://badge.fury.io/py/gitless.png)]( - http://badge.fury.io/py/gitless "PyPI version") -[![Homebrew Formula](https://img.shields.io/homebrew/v/gitless.svg)](http://braumeister.org/formula/gitless "Homebrew Formula") -[![Travis Build Status](https://travis-ci.org/sdg-mit/gitless.png?branch=master)](https://travis-ci.org/sdg-mit/gitless "Travis Build Status") -[![AppVeyor Build Status](https://ci.appveyor.com/api/projects/status/github/sdg-mit/gitless?svg=true)](https://ci.appveyor.com/project/spderosso/gitless "AppVeyor Build Status") - -[Gitless](http://gitless.com "Gitless's website") is an experimental version -control system built on top of Git. Many -people complain that Git is hard to use. We think the problem lies deeper than -the user interface, in the concepts underlying Git. Gitless is an experiment to -see what happens if you put a simple veneer on an app that changes the -underlying concepts. Because Gitless is implemented on top of Git (could be -considered what Git pros call a "porcelain" of Git), you can always fall -back on Git. And of course your coworkers you share a repository with need never -know that you're not a Git aficionado. +[![PyPI version](https://img.shields.io/pypi/v/gitless.svg)](https://pypi.org/project/gitless "PyPI version") +[![Homebrew Formula](https://img.shields.io/homebrew/v/gitless.svg)](https://formulae.brew.sh/formula/gitless "Homebrew Formula") + +[![Travis Build Status](https://img.shields.io/travis/gitless-vcs/gitless/master.svg)](https://travis-ci.org/gitless-vcs/gitless "Travis Build Status") +[![AppVeyor Build Status](https://ci.appveyor.com/api/projects/status/github/gitless-vcs/gitless?svg=true)](https://ci.appveyor.com/project/spderosso/gitless-11bfm "AppVeyor Build Status") + +[Gitless](http://gitless.com "Gitless's website") is a version control system built on top of Git, that is easy to learn and use: + +- **Simple commit workflow** + + Track or untrack files to control what changes to commit. Changes to tracked files are committed by default, but you can easily customize the set of files to commit using flags +- **Independent branches** + + Branches in Gitless include your working changes, so you can switch between branches without having to worry about conflicting uncommitted changes +- **Friendly command-line interface** + + Gitless commands will give you good feedback and help you figure out what to do next +- **Compatible with Git** + + Because Gitless is implemented on top of Git, you can always fall back on Git. And your coworkers you share a repo with need never know that you're not a Git aficionado. Moreover, you can use Gitless with GitHub or with any Git hosting service Install ------- -Note that the installation **won't interfere** with your Git installation in any -way, you can keep using Git, and switch between Git and Gitless seamlessly. +Installing Gitless won't interfere with your Git installation in any +way. You can keep using Git and switch between Git and Gitless seamlessly. -We currently require Git (1.7.12+) to be installed (but this requirement is -going to disappear soon once we finish with our migration to pygit2). +We currently require Git (1.7.12+) to be installed, but this requirement is +going to disappear soon once we finish with our migration to [pygit2](https://github.com/libgit2/pygit2). -### Binary releases (macOS and Linux only) +### Binary release (macOS and Linux only) Binary releases for macOS and Linux are available from the [Gitless website](http://gitless.com "Gitless's website"). @@ -45,14 +50,9 @@ If for some reason this doesn't work (maybe you are running an old version of your OS?), try one of the other options (installing from source or via the Python Package Index). - ### Installing from source -To install from source you need to have Python (2.7, 3.2+ or PyPy) -installed. - -Note to Windows users: Python 3 is not supported yet, -see [#146](https://github.com/sdg-mit/gitless/issues/146) for more info. +To install from source you need to have Python 3.7+ installed. Additionally, you need to [install pygit2]( http://www.pygit2.org/install.html "pygit2 install"). @@ -67,10 +67,7 @@ and do: If you are a Python fan you might find it easier to install Gitless via the Python Package Index. To do this, you need to have -Python (2.7, 3.2+ or PyPy) installed. - -Note to Windows users: Python 3 is not supported yet, -see [#146](https://github.com/sdg-mit/gitless/issues/146) for more info. +Python 3.7+ installed. Additionally, you need to [install pygit2]( http://www.pygit2.org/install.html "pygit2 install"). @@ -89,6 +86,28 @@ brew update brew install gitless ``` +### Installing via Snapcraft (Linux only) + +If you are using [Snapcraft](https://snapcraft.io/ "Snapcraft"), a +package manager for Linux, you can install the most recent release +of Gitless with: + +``` +snap install --channel=beta gitless +``` + +You can also use the `edge` channel to install the most recent build. + +### Installing via the Arch User Repository (Arch Linux only) + +If you are using [Arch Linux](https://www.archlinux.org/) or any of +its derivatives, you can use your favorite +[AUR Helper](https://wiki.archlinux.org/index.php/AUR_helpers) and install: +- [gitless](https://aur.archlinux.org/packages/gitless/) for the latest + released version +- [gitless-git](https://aur.archlinux.org/packages/gitless-git/) to + build the latest version straight from this repo + Documentation ------------- @@ -99,30 +118,24 @@ Documentation Contribute ---------- -There are several ways you can contribute to the project: - -- Bugs: did you find a bug? create an issue for it and we'll fix it -ASAP -- Code: you can browse through the open issues and see if there's something -there you would like to work on. Is something missing? feel free to propose it! -- Design: if you have any feedback about Gitless's design we would love to -hear from you. You can create an issue in the project with your -feedback/questions/suggestions or shoot us an email - - -If you're planning on submitting code here are some useful things to know: +If you find a bug, create an issue in our +GitHub repository. If you'd like to contribute +code, here are some useful things to know: -- We follow (to some extent) the [Google Python Style Guide]( +- To install gitless for development, [install pygit2]( + http://www.pygit2.org/install.html "pygit2 install"), clone the repo, + `cd` to the repo root and do `./setup.py develop`. This will install + the `gl` command with a symlink to your source files. You can make + changes to your code and run `gl` to test them. +- We follow, to some extent, the [Google Python Style Guide]( https://google.github.io/styleguide/pyguide.html "Google Python Style Guide"). Before submitting code, take a few seconds to look at the style guide and the -Gitless's code so that your edits are consistent with the codebase +Gitless code so that your edits are consistent with the codebase. - Finally, if you don't want [Travis]( - https://travis-ci.org/sdg-mit/gitless "Travis") to -be mad at you, check that tests pass in Python 2.7 and 3.2+. Tests can be run with + https://travis-ci.org/gitless-vcs/gitless "Travis") to +be mad at you, check that tests pass in Python 3.7+. Tests can be run with: ``` - pip install nose - nosetests # run tests other than end-to-end tests - nosetests ./gitless/tests/test_e2e.py # run end-to-end tests + python -m unittest discover gitless/tests ``` diff --git a/gitless.1 b/gitless.1 new file mode 100644 index 0000000..3ebc661 --- /dev/null +++ b/gitless.1 @@ -0,0 +1,664 @@ +.TH gitless "1" Manual +.SH NAME +gl +.SH SYNOPSIS +.B gl +[-h] [--version] {track,tr,untrack,un,status,st,diff,df,commit,ci,branch,br,tag,tg,checkout,co,merge,mg,resolve,rs,fuse,fs,remote,rt,publish,pb,switch,sw,init,in,history,hs} ... +.SH DESCRIPTION +Gitless: a version control system built on top of Git. +More info, downloads and documentation at http://gitless.com +.SH OPTIONS + +.TP +\fB\-\-version\fR +show program's version number and exit + +.SS +\fBSub-commands\fR +.TP +\fBgl\fR \fI\,track\/\fR +start tracking changes to files +.TP +\fBgl\fR \fI\,untrack\/\fR +stop tracking changes to files +.TP +\fBgl\fR \fI\,status\/\fR +show status of the repo +.TP +\fBgl\fR \fI\,diff\/\fR +show changes to files +.TP +\fBgl\fR \fI\,commit\/\fR +save changes to the local repository +.TP +\fBgl\fR \fI\,branch\/\fR +list, create, delete, or edit branches +.TP +\fBgl\fR \fI\,tag\/\fR +list, create, or delete tags +.TP +\fBgl\fR \fI\,checkout\/\fR +checkout committed versions of files +.TP +\fBgl\fR \fI\,merge\/\fR +merge the divergent changes of one branch onto another +.TP +\fBgl\fR \fI\,resolve\/\fR +mark files with conflicts as resolved +.TP +\fBgl\fR \fI\,fuse\/\fR +fuse the divergent changes of a branch onto the current branch +.TP +\fBgl\fR \fI\,remote\/\fR +list, create, edit or delete remotes +.TP +\fBgl\fR \fI\,publish\/\fR +publish commits upstream +.TP +\fBgl\fR \fI\,switch\/\fR +switch branches +.TP +\fBgl\fR \fI\,init\/\fR +create an empty git repository or clone remote +.TP +\fBgl\fR \fI\,history\/\fR +show commit history +.SH OPTIONS 'gl track' +usage: gl track [-h] files [files ...] + +Start tracking changes to files + +.TP +\fBfiles\fR +the file(s) to track + + +.SH OPTIONS 'gl tr' +usage: gl track [-h] files [files ...] + +Start tracking changes to files + +.TP +\fBfiles\fR +the file(s) to track + + +.SH OPTIONS 'gl untrack' +usage: gl untrack [-h] files [files ...] + +Stop tracking changes to files + +.TP +\fBfiles\fR +the file(s) to untrack + + +.SH OPTIONS 'gl un' +usage: gl untrack [-h] files [files ...] + +Stop tracking changes to files + +.TP +\fBfiles\fR +the file(s) to untrack + + +.SH OPTIONS 'gl status' +usage: gl status [-h] [paths [paths ...]] + +Show status of the repo + +.TP +\fBpaths\fR +the specific path(s) to status + + +.SH OPTIONS 'gl st' +usage: gl status [-h] [paths [paths ...]] + +Show status of the repo + +.TP +\fBpaths\fR +the specific path(s) to status + + +.SH OPTIONS 'gl diff' +usage: gl diff [-h] [-e file [file ...]] [-i file [file ...]] + [file [file ...]] + +Show changes to files. By default all tracked modified files are diffed. To customize the set of files to diff use the only, exclude, and include flags + +.TP +\fBfile\fR +use only files given (tracked modified or untracked) + +.TP +\fB\-e\fR file [file ...], \fB\-\-exclude\fR file [file ...] +exclude files given (files must be tracked modified) + +.TP +\fB\-i\fR file [file ...], \fB\-\-include\fR file [file ...] +include files given (files must be untracked) + +.SH OPTIONS 'gl df' +usage: gl diff [-h] [-e file [file ...]] [-i file [file ...]] + [file [file ...]] + +Show changes to files. By default all tracked modified files are diffed. To customize the set of files to diff use the only, exclude, and include flags + +.TP +\fBfile\fR +use only files given (tracked modified or untracked) + +.TP +\fB\-e\fR file [file ...], \fB\-\-exclude\fR file [file ...] +exclude files given (files must be tracked modified) + +.TP +\fB\-i\fR file [file ...], \fB\-\-include\fR file [file ...] +include files given (files must be untracked) + +.SH OPTIONS 'gl commit' +usage: gl commit [-h] [-m M] [-p] [-e file [file ...]] + [-i file [file ...]] + [file [file ...]] + +Save changes to the local repository. By default all tracked modified files are committed. To customize the set of files to be committed use the only, exclude, and include flags + +.TP +\fBfile\fR +use only files given (tracked modified or untracked) + +.TP +\fB\-m\fR \fI\,M\/\fR, \fB\-\-message\fR \fI\,M\/\fR +Commit message + +.TP +\fB\-p\fR, \fB\-\-partial\fR +Interactively select segments of files to commit + +.TP +\fB\-e\fR file [file ...], \fB\-\-exclude\fR file [file ...] +exclude files given (files must be tracked modified) + +.TP +\fB\-i\fR file [file ...], \fB\-\-include\fR file [file ...] +include files given (files must be untracked) + +.SH OPTIONS 'gl ci' +usage: gl commit [-h] [-m M] [-p] [-e file [file ...]] + [-i file [file ...]] + [file [file ...]] + +Save changes to the local repository. By default all tracked modified files are committed. To customize the set of files to be committed use the only, exclude, and include flags + +.TP +\fBfile\fR +use only files given (tracked modified or untracked) + +.TP +\fB\-m\fR \fI\,M\/\fR, \fB\-\-message\fR \fI\,M\/\fR +Commit message + +.TP +\fB\-p\fR, \fB\-\-partial\fR +Interactively select segments of files to commit + +.TP +\fB\-e\fR file [file ...], \fB\-\-exclude\fR file [file ...] +exclude files given (files must be tracked modified) + +.TP +\fB\-i\fR file [file ...], \fB\-\-include\fR file [file ...] +include files given (files must be untracked) + +.SH OPTIONS 'gl branch' +usage: gl branch [-h] [-r] [-v] [-c branch [branch ...]] + [-dp DP] [-d branch [branch ...]] + [-sh commit_id] [-su branch] [-uu] + [-rn RENAME_B [RENAME_B ...]] + +List, create, delete, or edit branches + + + +.TP +\fB\-r\fR, \fB\-\-remote\fR +list remote branches in addition to local branches + +.TP +\fB\-v\fR, \fB\-\-verbose\fR +be verbose, will output the head of each branch + +.TP +\fB\-c\fR branch [branch ...], \fB\-\-create\fR branch [branch ...] +create branch(es) + +.TP +\fB\-dp\fR \fI\,DP\/\fR, \fB\-\-divergent\-point\fR \fI\,DP\/\fR +the commit from where to 'branch out' (only relevant if a new branch is created; defaults to HEAD) + +.TP +\fB\-d\fR branch [branch ...], \fB\-\-delete\fR branch [branch ...] +delete branch(es) + +.TP +\fB\-sh\fR commit_id, \fB\-\-set\-head\fR commit_id +set the head of the current branch + +.TP +\fB\-su\fR branch, \fB\-\-set\-upstream\fR branch +set the upstream branch of the current branch + +.TP +\fB\-uu\fR, \fB\-\-unset\-upstream\fR +unset the upstream branch of the current branch + +.TP +\fB\-rn\fR \fI\,RENAME_B\/\fR [\fI\,RENAME_B\/\fR ...], \fB\-\-rename\-branch\fR \fI\,RENAME_B\/\fR [\fI\,RENAME_B\/\fR ...] +renames the current branch (gl branch \-rn new_name) or another specified branch (gl branch \-rn branch_name new_name) + +.SH OPTIONS 'gl br' +usage: gl branch [-h] [-r] [-v] [-c branch [branch ...]] + [-dp DP] [-d branch [branch ...]] + [-sh commit_id] [-su branch] [-uu] + [-rn RENAME_B [RENAME_B ...]] + +List, create, delete, or edit branches + + + +.TP +\fB\-r\fR, \fB\-\-remote\fR +list remote branches in addition to local branches + +.TP +\fB\-v\fR, \fB\-\-verbose\fR +be verbose, will output the head of each branch + +.TP +\fB\-c\fR branch [branch ...], \fB\-\-create\fR branch [branch ...] +create branch(es) + +.TP +\fB\-dp\fR \fI\,DP\/\fR, \fB\-\-divergent\-point\fR \fI\,DP\/\fR +the commit from where to 'branch out' (only relevant if a new branch is created; defaults to HEAD) + +.TP +\fB\-d\fR branch [branch ...], \fB\-\-delete\fR branch [branch ...] +delete branch(es) + +.TP +\fB\-sh\fR commit_id, \fB\-\-set\-head\fR commit_id +set the head of the current branch + +.TP +\fB\-su\fR branch, \fB\-\-set\-upstream\fR branch +set the upstream branch of the current branch + +.TP +\fB\-uu\fR, \fB\-\-unset\-upstream\fR +unset the upstream branch of the current branch + +.TP +\fB\-rn\fR \fI\,RENAME_B\/\fR [\fI\,RENAME_B\/\fR ...], \fB\-\-rename\-branch\fR \fI\,RENAME_B\/\fR [\fI\,RENAME_B\/\fR ...] +renames the current branch (gl branch \-rn new_name) or another specified branch (gl branch \-rn branch_name new_name) + +.SH OPTIONS 'gl tag' +usage: gl tag [-h] [-r] [-c tag [tag ...]] [-ci CI] + [-d tag [tag ...]] + +List, create, or delete tags + + + +.TP +\fB\-r\fR, \fB\-\-remote\fR +list remote tags in addition to local tags + +.TP +\fB\-c\fR tag [tag ...], \fB\-\-create\fR tag [tag ...] +create tag(s) + +.TP +\fB\-ci\fR \fI\,CI\/\fR, \fB\-\-commit\fR \fI\,CI\/\fR +the commit to tag (only relevant if a new tag is created; defaults to the HEAD commit) + +.TP +\fB\-d\fR tag [tag ...], \fB\-\-delete\fR tag [tag ...] +delete tag(s) + +.SH OPTIONS 'gl tg' +usage: gl tag [-h] [-r] [-c tag [tag ...]] [-ci CI] + [-d tag [tag ...]] + +List, create, or delete tags + + + +.TP +\fB\-r\fR, \fB\-\-remote\fR +list remote tags in addition to local tags + +.TP +\fB\-c\fR tag [tag ...], \fB\-\-create\fR tag [tag ...] +create tag(s) + +.TP +\fB\-ci\fR \fI\,CI\/\fR, \fB\-\-commit\fR \fI\,CI\/\fR +the commit to tag (only relevant if a new tag is created; defaults to the HEAD commit) + +.TP +\fB\-d\fR tag [tag ...], \fB\-\-delete\fR tag [tag ...] +delete tag(s) + +.SH OPTIONS 'gl checkout' +usage: gl checkout [-h] [-cp CP] files [files ...] + +Checkout committed versions of files + +.TP +\fBfiles\fR +the file(s) to checkout + +.TP +\fB\-cp\fR \fI\,CP\/\fR, \fB\-\-commit\-point\fR \fI\,CP\/\fR +the commit point to checkout the files at. Defaults to HEAD. + +.SH OPTIONS 'gl co' +usage: gl checkout [-h] [-cp CP] files [files ...] + +Checkout committed versions of files + +.TP +\fBfiles\fR +the file(s) to checkout + +.TP +\fB\-cp\fR \fI\,CP\/\fR, \fB\-\-commit\-point\fR \fI\,CP\/\fR +the commit point to checkout the files at. Defaults to HEAD. + +.SH OPTIONS 'gl merge' +usage: gl merge [-h] [-a] [src] + +Merge the divergent changes of one branch onto another + +.TP +\fBsrc\fR +the source branch to read changes from + +.TP +\fB\-a\fR, \fB\-\-abort\fR +abort the merge in progress + +.SH OPTIONS 'gl mg' +usage: gl merge [-h] [-a] [src] + +Merge the divergent changes of one branch onto another + +.TP +\fBsrc\fR +the source branch to read changes from + +.TP +\fB\-a\fR, \fB\-\-abort\fR +abort the merge in progress + +.SH OPTIONS 'gl resolve' +usage: gl resolve [-h] files [files ...] + +Mark files with conflicts as resolved + +.TP +\fBfiles\fR +the file(s) to resolve + + +.SH OPTIONS 'gl rs' +usage: gl resolve [-h] files [files ...] + +Mark files with conflicts as resolved + +.TP +\fBfiles\fR +the file(s) to resolve + + +.SH OPTIONS 'gl fuse' +usage: gl fuse [-h] [-o commit_id [commit_id ...]] + [-e commit_id [commit_id ...]] [-ip [commit_id]] + [-a] + [src] + +Fuse the divergent changes of a branch onto the current branch. By default all divergent changes from the given source branch are fused. To customize the set of commits to fuse use the only and exclude flags + +.TP +\fBsrc\fR +the source branch to read changes from. If none is given the upstream branch of the current branch is used as the source + +.TP +\fB\-o\fR commit_id [commit_id ...], \fB\-\-only\fR commit_id [commit_id ...] +fuse only the commits given (commits must belong to the set of divergent commits from the given src branch) + +.TP +\fB\-e\fR commit_id [commit_id ...], \fB\-\-exclude\fR commit_id [commit_id ...] +exclude from the fuse the commits given (commits must belong to the set of divergent commits from the given src branch) + +.TP +\fB\-ip\fR [commit_id], \fB\-\-insertion\-point\fR [commit_id] +the divergent changes will be inserted after the commit given, dp for divergent point is the default + +.TP +\fB\-a\fR, \fB\-\-abort\fR +abort the fuse in progress + +.SH OPTIONS 'gl fs' +usage: gl fuse [-h] [-o commit_id [commit_id ...]] + [-e commit_id [commit_id ...]] [-ip [commit_id]] + [-a] + [src] + +Fuse the divergent changes of a branch onto the current branch. By default all divergent changes from the given source branch are fused. To customize the set of commits to fuse use the only and exclude flags + +.TP +\fBsrc\fR +the source branch to read changes from. If none is given the upstream branch of the current branch is used as the source + +.TP +\fB\-o\fR commit_id [commit_id ...], \fB\-\-only\fR commit_id [commit_id ...] +fuse only the commits given (commits must belong to the set of divergent commits from the given src branch) + +.TP +\fB\-e\fR commit_id [commit_id ...], \fB\-\-exclude\fR commit_id [commit_id ...] +exclude from the fuse the commits given (commits must belong to the set of divergent commits from the given src branch) + +.TP +\fB\-ip\fR [commit_id], \fB\-\-insertion\-point\fR [commit_id] +the divergent changes will be inserted after the commit given, dp for divergent point is the default + +.TP +\fB\-a\fR, \fB\-\-abort\fR +abort the fuse in progress + +.SH OPTIONS 'gl remote' +usage: gl remote [-h] [-c [remote]] [-d remote [remote ...]] + [-rn RENAME_R [RENAME_R ...]] + [remote_url] + +List, create, edit or delete remotes + +.TP +\fBremote_url\fR +the url of the remote (only relevant if a new remote is created) + +.TP +\fB\-c\fR [remote], \fB\-\-create\fR [remote] +create remote + +.TP +\fB\-d\fR remote [remote ...], \fB\-\-delete\fR remote [remote ...] +delete remote(es) + +.TP +\fB\-rn\fR \fI\,RENAME_R\/\fR [\fI\,RENAME_R\/\fR ...], \fB\-\-rename\fR \fI\,RENAME_R\/\fR [\fI\,RENAME_R\/\fR ...] +renames the specified remote: accepts two arguments (current remote name and new remote name) + +.SH OPTIONS 'gl rt' +usage: gl remote [-h] [-c [remote]] [-d remote [remote ...]] + [-rn RENAME_R [RENAME_R ...]] + [remote_url] + +List, create, edit or delete remotes + +.TP +\fBremote_url\fR +the url of the remote (only relevant if a new remote is created) + +.TP +\fB\-c\fR [remote], \fB\-\-create\fR [remote] +create remote + +.TP +\fB\-d\fR remote [remote ...], \fB\-\-delete\fR remote [remote ...] +delete remote(es) + +.TP +\fB\-rn\fR \fI\,RENAME_R\/\fR [\fI\,RENAME_R\/\fR ...], \fB\-\-rename\fR \fI\,RENAME_R\/\fR [\fI\,RENAME_R\/\fR ...] +renames the specified remote: accepts two arguments (current remote name and new remote name) + +.SH OPTIONS 'gl publish' +usage: gl publish [-h] [dst] + +Publish commits upstream + +.TP +\fBdst\fR +the branch where to publish commits + + +.SH OPTIONS 'gl pb' +usage: gl publish [-h] [dst] + +Publish commits upstream + +.TP +\fBdst\fR +the branch where to publish commits + + +.SH OPTIONS 'gl switch' +usage: gl switch [-h] [-mo] branch + +Switch branches + +.TP +\fBbranch\fR +switch to branch + +.TP +\fB\-mo\fR, \fB\-\-move\-over\fR +move uncomitted changes made in the current branch to the destination branch + +.SH OPTIONS 'gl sw' +usage: gl switch [-h] [-mo] branch + +Switch branches + +.TP +\fBbranch\fR +switch to branch + +.TP +\fB\-mo\fR, \fB\-\-move\-over\fR +move uncomitted changes made in the current branch to the destination branch + +.SH OPTIONS 'gl init' +usage: gl init [-h] [-o ONLY [ONLY ...]] + [-e EXCLUDE [EXCLUDE ...]] + [repo] + +Create an empty git repository or clone remote + +.TP +\fBrepo\fR +an optional remote repo address from where to read to create the local repo + +.TP +\fB\-o\fR \fI\,ONLY\/\fR [\fI\,ONLY\/\fR ...], \fB\-\-only\fR \fI\,ONLY\/\fR [\fI\,ONLY\/\fR ...] +use only branches given from remote repo + +.TP +\fB\-e\fR \fI\,EXCLUDE\/\fR [\fI\,EXCLUDE\/\fR ...], \fB\-\-exclude\fR \fI\,EXCLUDE\/\fR [\fI\,EXCLUDE\/\fR ...] +use everything but these branches from remote repo + +.SH OPTIONS 'gl in' +usage: gl init [-h] [-o ONLY [ONLY ...]] + [-e EXCLUDE [EXCLUDE ...]] + [repo] + +Create an empty git repository or clone remote + +.TP +\fBrepo\fR +an optional remote repo address from where to read to create the local repo + +.TP +\fB\-o\fR \fI\,ONLY\/\fR [\fI\,ONLY\/\fR ...], \fB\-\-only\fR \fI\,ONLY\/\fR [\fI\,ONLY\/\fR ...] +use only branches given from remote repo + +.TP +\fB\-e\fR \fI\,EXCLUDE\/\fR [\fI\,EXCLUDE\/\fR ...], \fB\-\-exclude\fR \fI\,EXCLUDE\/\fR [\fI\,EXCLUDE\/\fR ...] +use everything but these branches from remote repo + +.SH OPTIONS 'gl history' +usage: gl history [-h] [-v] [-l LIMIT] [-c] [-b [branch_name]] + +Show commit history + + +.TP +\fB\-v\fR, \fB\-\-verbose\fR +be verbose, will output the diffs of the commit + +.TP +\fB\-l\fR \fI\,LIMIT\/\fR, \fB\-\-limit\fR \fI\,LIMIT\/\fR +limit number of commits displayed + +.TP +\fB\-c\fR, \fB\-\-compact\fR +output history in a compact format + +.TP +\fB\-b\fR [branch_name], \fB\-\-branch\fR [branch_name] +the branch to show history of (defaults to the current branch) + +.SH OPTIONS 'gl hs' +usage: gl history [-h] [-v] [-l LIMIT] [-c] [-b [branch_name]] + +Show commit history + + +.TP +\fB\-v\fR, \fB\-\-verbose\fR +be verbose, will output the diffs of the commit + +.TP +\fB\-l\fR \fI\,LIMIT\/\fR, \fB\-\-limit\fR \fI\,LIMIT\/\fR +limit number of commits displayed + +.TP +\fB\-c\fR, \fB\-\-compact\fR +output history in a compact format + +.TP +\fB\-b\fR [branch_name], \fB\-\-branch\fR [branch_name] +the branch to show history of (defaults to the current branch) + +.SH AUTHORS +.B gitless +was written by Santiago Perez De Rosso . +.SH DISTRIBUTION +The latest version of gitless may be downloaded from +.UR http://gitless.com +.UE diff --git a/gitless/cli/commit_dialog.py b/gitless/cli/commit_dialog.py index 891fd42..872f6ce 100644 --- a/gitless/cli/commit_dialog.py +++ b/gitless/cli/commit_dialog.py @@ -5,8 +5,6 @@ """Gitless's commit dialog.""" -from __future__ import unicode_literals - import io from locale import getpreferredencoding import os @@ -18,7 +16,6 @@ from . import pprint -IS_PY2 = sys.version_info[0] == 2 ENCODING = getpreferredencoding() or 'utf-8' _COMMIT_FILE = 'GL_COMMIT_EDIT_MSG' @@ -35,11 +32,7 @@ def show(files, repo): Returns: The commit msg. """ - if IS_PY2: - # wb because we use pprint to write - cf = io.open(_commit_file(repo), mode='wb') - else: - cf = io.open(_commit_file(repo), mode='w', encoding=ENCODING) + cf = io.open(_commit_file(repo), mode='w', encoding=ENCODING) curr_b = repo.current_branch if curr_b.merge_in_progress or curr_b.fuse_in_progress: diff --git a/gitless/cli/file_cmd.py b/gitless/cli/file_cmd.py index f5f3120..d184319 100644 --- a/gitless/cli/file_cmd.py +++ b/gitless/cli/file_cmd.py @@ -5,18 +5,16 @@ """Helper module for gl_{track, untrack, resolve}.""" -from __future__ import unicode_literals - from . import helpers, pprint VOWELS = ('a', 'e', 'i', 'o', 'u') -def parser(help_msg, subcmd): +def parser(help_msg, subcmd, subcmd_aliases=[]): def f(subparsers, repo): p = subparsers.add_parser( - subcmd, help=help_msg, description=help_msg.capitalize()) + subcmd, help=help_msg, description=help_msg.capitalize(), aliases=subcmd_aliases) p.add_argument( 'files', nargs='+', help='the file(s) to {0}'.format(subcmd), action=helpers.PathProcessor, repo=repo, diff --git a/gitless/cli/gl.py b/gitless/cli/gl.py index e5be6c7..67bed8a 100644 --- a/gitless/cli/gl.py +++ b/gitless/cli/gl.py @@ -5,19 +5,13 @@ """gl - Main Gitless's command. Dispatcher to the other cmds.""" -from __future__ import unicode_literals - import sys import argparse +import argcomplete import traceback import pygit2 -if sys.platform != 'win32': - from sh import ErrorReturnCode -else: - from pbs import ErrorReturnCode - -from clint.textui import colored +from subprocess import CalledProcessError from gitless import core @@ -26,6 +20,7 @@ gl_checkout, gl_merge, gl_resolve, gl_fuse, gl_remote, gl_publish, gl_switch, gl_init, gl_history) from . import pprint +from . import helpers SUCCESS = 0 @@ -34,7 +29,7 @@ INTERNAL_ERROR = 3 NOT_IN_GL_REPO = 4 -__version__ = '0.8.6' +__version__ = '0.8.8' URL = 'http://gitless.com' @@ -42,9 +37,9 @@ try: repo = core.Repository() try: - colored.DISABLE_COLOR = not repo.config.get_bool('color.ui') + pprint.DISABLE_COLOR = not repo.config.get_bool('color.ui') except pygit2.GitError: - colored.DISABLE_COLOR = ( + pprint.DISABLE_COLOR = ( repo.config['color.ui'] in ['no', 'never']) except (core.NotInRepoError, KeyError): pass @@ -67,12 +62,14 @@ def print_help(parser): for choice in subparsers_action._choices_actions: print(' {:<19} {}'.format(choice.dest, choice.help)) -def main(): +def build_parser(subcommands, repo): parser = argparse.ArgumentParser( description=( 'Gitless: a version control system built on top of Git.\nMore info, ' 'downloads and documentation at {0}'.format(URL)), formatter_class=argparse.RawDescriptionHelpFormatter) + if sys.version_info[0] < 3: + parser.register('action', 'parsers', helpers.AliasedSubParsersAction) parser.add_argument( '--version', action='version', version=( 'GL Version: {0}\nYou can check if there\'s a new version of Gitless ' @@ -80,13 +77,25 @@ def main(): subparsers = parser.add_subparsers(title='subcommands', dest='subcmd_name') subparsers.required = True + for sub_cmd in subcommands: + sub_cmd.parser(subparsers, repo) + + return parser + +def setup_windows_console(): + if sys.platform == 'win32': + import ctypes + kernel32 = ctypes.windll.kernel32 + kernel32.SetConsoleMode(kernel32.GetStdHandle(-11), 7) + +def main(): sub_cmds = [ gl_track, gl_untrack, gl_status, gl_diff, gl_commit, gl_branch, gl_tag, gl_checkout, gl_merge, gl_resolve, gl_fuse, gl_remote, gl_publish, gl_switch, gl_init, gl_history] - for sub_cmd in sub_cmds: - sub_cmd.parser(subparsers, repo) + parser = build_parser(sub_cmds, repo) + argcomplete.autocomplete(parser) if len(sys.argv) == 1: print_help(parser) return SUCCESS @@ -96,6 +105,7 @@ def main(): if args.subcmd_name != 'init' and not repo: raise core.NotInRepoError('You are not in a Gitless\'s repository') + setup_windows_console() return SUCCESS if args.func(args, repo) else ERRORS_FOUND except KeyboardInterrupt: pprint.puts('\n') @@ -109,7 +119,7 @@ def main(): except (ValueError, pygit2.GitError, core.GlError) as e: pprint.err(e) return ERRORS_FOUND - except ErrorReturnCode as e: + except CalledProcessError as e: pprint.err(e.stderr) return ERRORS_FOUND except: diff --git a/gitless/cli/gl_branch.py b/gitless/cli/gl_branch.py index f5032a0..bd3b2bb 100644 --- a/gitless/cli/gl_branch.py +++ b/gitless/cli/gl_branch.py @@ -5,10 +5,6 @@ """gl branch - List, create, edit or delete branches.""" -from __future__ import unicode_literals - -from clint.textui import colored - from gitless import core from . import helpers, pprint @@ -18,7 +14,7 @@ def parser(subparsers, _): """Adds the branch parser to the given subparsers object.""" desc = 'list, create, delete, or edit branches' branch_parser = subparsers.add_parser( - 'branch', help=desc, description=desc.capitalize()) + 'branch', help=desc, description=desc.capitalize(), aliases=['br']) list_group = branch_parser.add_argument_group('list branches') list_group.add_argument( @@ -43,19 +39,27 @@ def parser(subparsers, _): '-d', '--delete', nargs='+', help='delete branch(es)', dest='delete_b', metavar='branch') - edit_group = branch_parser.add_argument_group('edit the current branch') - edit_group.add_argument( + edit_current_branch_group = branch_parser.add_argument_group('edit the current branch') + edit_current_branch_group.add_argument( '-sh', '--set-head', help='set the head of the current branch', dest='new_head', metavar='commit_id') - edit_group.add_argument( + edit_current_branch_group.add_argument( '-su', '--set-upstream', help='set the upstream branch of the current branch', dest='upstream_b', metavar='branch') - edit_group.add_argument( + edit_current_branch_group.add_argument( '-uu', '--unset-upstream', help='unset the upstream branch of the current branch', action='store_true') + edit_group = branch_parser.add_argument_group('edit branches') + edit_group.add_argument( + '-rn', '--rename-branch', nargs='+', + help='renames the current branch (gl branch -rn new_name) ' + 'or another specified branch (gl branch -rn branch_name new_name)', + dest='rename_b' + ) + branch_parser.set_defaults(func=main) @@ -63,7 +67,7 @@ def main(args, repo): is_list = bool(args.verbose or args.remote) is_create = bool(args.create_b or args.dp) is_delete = bool(args.delete_b) - is_edit = bool(args.new_head or args.upstream_b or args.unset_upstream) + is_edit = bool(args.new_head or args.upstream_b or args.unset_upstream or args.rename_b) if is_list + is_create + is_delete + is_edit > 1: pprint.err('Invalid flag combination') @@ -82,6 +86,8 @@ def main(args, repo): ret = _do_unset_upstream(repo) elif args.new_head: ret = _do_set_head(args.new_head, repo) + elif args.rename_b: + ret = _do_rename(args.rename_b, repo) else: _do_list(repo, args.remote, v=args.verbose) @@ -100,7 +106,7 @@ def _do_list(repo, list_remote, v=False): for b in (repo.lookup_branch(n) for n in sorted(repo.listall_branches())): current_str = '*' if b.is_current else ' ' upstream_str = '(upstream is {0})'.format(b.upstream) if b.upstream else '' - color = colored.green if b.is_current else colored.yellow + color = pprint.green if b.is_current else pprint.yellow pprint.item( '{0} {1} {2}'.format(current_str, color(b.branch_name), upstream_str)) if v: @@ -108,8 +114,10 @@ def _do_list(repo, list_remote, v=False): if list_remote: for r in sorted(repo.remotes, key=lambda r: r.name): - for b in (r.lookup_branch(n) for n in sorted(r.listall_branches())): - pprint.item(' {0}'.format(colored.yellow(str(b)))) + branches = r.lookupall_branches() if v else r.listall_branches() + b_remote = '' if v else r.name + '/' + for b in branches: + pprint.item(' {0}'.format(pprint.yellow(b_remote + str(b)))) if v: pprint.item(' ➜ head is {0}'.format(pprint.commit_str(b.head))) @@ -139,8 +147,14 @@ def _do_create(create_b, dp, repo): continue remote_str = ' in remote repository {0}'.format(maybe_remote) try: - r.create_branch(b_name, target) + new_branch = r.create_branch(b_name, target) pprint.ok('Created new branch {0}{1}'.format(b_name, remote_str)) + try: + new_branch.upstream = helpers.get_branch(dp, repo) + pprint.ok('Upstream of {0} set to {1}'.format(b_name, dp)) + except: + # Not a branch + continue except ValueError as e: pprint.err(e) errors_found = True @@ -203,3 +217,23 @@ def _do_set_head(commit_id, repo): pprint.ok( 'Head of current branch {0} is now {1}'.format(curr_b, pprint.commit_str(commit))) return True + + +def _do_rename(rename_b, repo): + ret = True + if len(rename_b) == 1 : + # Renaming the current branch + curr_b = repo.current_branch + curr_b.rename(rename_b[0]) + pprint.ok('Renamed this branch to {0}'.format(rename_b[0])) + elif len(rename_b) == 2: + # Renaming a specified branch to a new name + b = helpers.get_branch(rename_b[0], repo) + b.rename(rename_b[1]) + pprint.ok('Renamed branch {0} to {1}'.format(rename_b[0], rename_b[1])) + else : + # Gave more than 2 arguments + pprint.err( + 'Too many arguments given. Expected 1 or 2 arguments.') + ret = False + return ret diff --git a/gitless/cli/gl_checkout.py b/gitless/cli/gl_checkout.py index 74141da..b0c143b 100644 --- a/gitless/cli/gl_checkout.py +++ b/gitless/cli/gl_checkout.py @@ -5,8 +5,6 @@ """gl checkout - Checkout committed versions of files.""" -from __future__ import unicode_literals - from gitless import core from . import helpers, pprint @@ -16,7 +14,7 @@ def parser(subparsers, repo): """Adds the checkout parser to the given subparsers object.""" desc = 'checkout committed versions of files' checkout_parser = subparsers.add_parser( - 'checkout', help=desc, description=desc.capitalize()) + 'checkout', help=desc, description=desc.capitalize(), aliases=['co']) checkout_parser.add_argument( '-cp', '--commit-point', help=( 'the commit point to checkout the files at. Defaults to HEAD.'), diff --git a/gitless/cli/gl_commit.py b/gitless/cli/gl_commit.py index df39901..58798da 100644 --- a/gitless/cli/gl_commit.py +++ b/gitless/cli/gl_commit.py @@ -5,17 +5,6 @@ """gl commit - Record changes in the local repository.""" -from __future__ import unicode_literals - -import subprocess - -import sys -if sys.platform != 'win32': - from sh import git -else: - from pbs import Command - git = Command('git') - from gitless import core from . import commit_dialog @@ -30,7 +19,7 @@ def parser(subparsers, repo): desc.capitalize() + '. ' + 'By default all tracked modified files are committed. To customize the' ' set of files to be committed use the only, exclude, and include ' - 'flags')) + 'flags'), aliases=['ci']) commit_parser.add_argument( '-m', '--message', help='Commit message', dest='m') commit_parser.add_argument( @@ -50,6 +39,20 @@ def main(args, repo): return False curr_b = repo.current_branch + total_additions = 0 + total_deletions = 0 + for fp in commit_files: + try: + patch = curr_b.diff_file(fp) + except KeyError: + continue + + if patch.delta.is_binary: + continue + + total_additions += patch.line_stats[1] + total_deletions += patch.line_stats[2] + partials = None if args.p: partials = _do_partial_selection(commit_files, curr_b) @@ -60,7 +63,7 @@ def main(args, repo): msg = args.m if args.m else commit_dialog.show(commit_files, repo) if not msg.strip(): if partials: - git.reset('HEAD', partials) + core.git('reset', 'HEAD', partials) raise ValueError('Missing commit message') _auto_track(commit_files, curr_b) @@ -68,7 +71,7 @@ def main(args, repo): pprint.ok('Commit on branch {0} succeeded'.format(repo.current_branch)) pprint.blank() - pprint.commit(ci) + pprint.commit(ci, line_additions=total_additions, line_deletions=total_deletions) if curr_b.fuse_in_progress: _op_continue(curr_b.fuse_continue, 'Fuse') @@ -107,7 +110,7 @@ def _do_partial_selection(files, curr_b): pprint.warn('Can\'t select segments for deleted file {0}'.format(fp)) continue - subprocess.call(['git', 'add', '-p', fp]) + core.git('add', '-p', fp) # TODO: check that at least one hunk was staged partials.append(fp) diff --git a/gitless/cli/gl_diff.py b/gitless/cli/gl_diff.py index 169c0f0..36346d0 100644 --- a/gitless/cli/gl_diff.py +++ b/gitless/cli/gl_diff.py @@ -5,8 +5,6 @@ """gl diff - Show changes in files.""" -from __future__ import unicode_literals - import os import tempfile @@ -20,7 +18,7 @@ def parser(subparsers, repo): 'diff', help=desc, description=( desc.capitalize() + '. ' + 'By default all tracked modified files are diffed. To customize the ' - ' set of files to diff use the only, exclude, and include flags')) + ' set of files to diff use the only, exclude, and include flags'), aliases=['df']) helpers.oei_flags(diff_parser, repo) diff_parser.set_defaults(func=main) @@ -33,6 +31,9 @@ def main(args, repo): success = True curr_b = repo.current_branch with tempfile.NamedTemporaryFile(mode='w', delete=False) as tf: + total_additions = 0 + total_deletions = 0 + patches = [] for fp in files: try: patch = curr_b.diff_file(fp) @@ -47,11 +48,16 @@ def main(args, repo): additions = patch.line_stats[1] deletions = patch.line_stats[2] + total_additions += additions + total_deletions += deletions if (not additions) and (not deletions): pprint.warn('No diffs to output for {0}'.format(fp)) continue - - pprint.diff(patch, stream=tf.write) + patches.append(patch) + if patches: + pprint.diff_totals(total_additions, total_deletions, stream=tf.write) + for patch in patches: + pprint.diff(patch, stream=tf.write) if os.path.getsize(tf.name) > 0: helpers.page(tf.name, repo) diff --git a/gitless/cli/gl_fuse.py b/gitless/cli/gl_fuse.py index 3863320..864f2c9 100644 --- a/gitless/cli/gl_fuse.py +++ b/gitless/cli/gl_fuse.py @@ -5,8 +5,6 @@ """gl fuse - Fuse the divergent changes of a branch onto the current branch.""" -from __future__ import unicode_literals - from gitless import core from . import helpers, pprint @@ -19,7 +17,7 @@ def parser(subparsers, repo): desc.capitalize() + '. ' + 'By default all divergent changes from the given source branch are ' 'fused. To customize the set of commits to fuse use the only and ' - 'exclude flags')) + 'exclude flags'), aliases=['fs']) fuse_parser.add_argument( 'src', nargs='?', help=( diff --git a/gitless/cli/gl_history.py b/gitless/cli/gl_history.py index 9c648d8..a2a1108 100644 --- a/gitless/cli/gl_history.py +++ b/gitless/cli/gl_history.py @@ -5,8 +5,6 @@ """gl history - Show commit history.""" -from __future__ import unicode_literals - import os import tempfile @@ -17,7 +15,7 @@ def parser(subparsers, _): """Adds the history parser to the given subparsers object.""" desc = 'show commit history' history_parser = subparsers.add_parser( - 'history', help=desc, description=desc.capitalize()) + 'history', help=desc, description=desc.capitalize(), aliases=['hs']) history_parser.add_argument( '-v', '--verbose', help='be verbose, will output the diffs of the commit', action='store_true') diff --git a/gitless/cli/gl_init.py b/gitless/cli/gl_init.py index eb6bbde..a248f62 100644 --- a/gitless/cli/gl_init.py +++ b/gitless/cli/gl_init.py @@ -5,8 +5,6 @@ """gl init - Create an empty repo or make a clone.""" -from __future__ import unicode_literals - import os from gitless import core @@ -17,15 +15,21 @@ def parser(subparsers, _): """Adds the init parser to the given subparsers object.""" desc = ( - 'create an empty Gitless\'s repository or create one from an existing ' - 'remote repository') + 'create an empty git repository or clone remote') init_parser = subparsers.add_parser( - 'init', help=desc, description=desc.capitalize()) + 'init', help=desc, description=desc.capitalize(), aliases=['in']) init_parser.add_argument( 'repo', nargs='?', help=( 'an optional remote repo address from where to read to create the ' 'local repo')) + init_parser.add_argument( + '-o', '--only', nargs='+', + help='use only branches given from remote repo', dest='only') + init_parser.add_argument( + '-e', '--exclude', nargs='+', + help='use everything but these branches from remote repo', dest='exclude') + init_parser.set_defaults(func=main) @@ -33,7 +37,9 @@ def main(args, repo): if repo: pprint.err('You are already in a Gitless repository') return False - core.init_repository(url=args.repo) + core.init_repository(url=args.repo, + only=frozenset(args.only if args.only else []), + exclude=frozenset(args.exclude if args.exclude else [])) pprint.ok('Local repo created in {0}'.format(os.getcwd())) if args.repo: pprint.ok('Initialized from remote {0}'.format(args.repo)) diff --git a/gitless/cli/gl_merge.py b/gitless/cli/gl_merge.py index a37ad49..118de4d 100644 --- a/gitless/cli/gl_merge.py +++ b/gitless/cli/gl_merge.py @@ -5,8 +5,6 @@ """gl merge - Merge the divergent changes of one branch onto another.""" -from __future__ import unicode_literals - from gitless import core from . import helpers, pprint @@ -15,7 +13,7 @@ def parser(subparsers, repo): desc = 'merge the divergent changes of one branch onto another' merge_parser = subparsers.add_parser( - 'merge', help=desc, description=desc.capitalize()) + 'merge', help=desc, description=desc.capitalize(), aliases=['mg']) group = merge_parser.add_mutually_exclusive_group() group.add_argument( 'src', nargs='?', help='the source branch to read changes from') diff --git a/gitless/cli/gl_publish.py b/gitless/cli/gl_publish.py index f99bdb9..19b6227 100644 --- a/gitless/cli/gl_publish.py +++ b/gitless/cli/gl_publish.py @@ -5,8 +5,6 @@ """gl publish - Publish commits upstream.""" -from __future__ import unicode_literals - from . import helpers, pprint @@ -14,7 +12,7 @@ def parser(subparsers, _): """Adds the publish parser to the given subparsers object.""" desc = 'publish commits upstream' publish_parser = subparsers.add_parser( - 'publish', help=desc, description=desc.capitalize()) + 'publish', help=desc, description=desc.capitalize(), aliases=['pb']) publish_parser.add_argument( 'dst', nargs='?', help='the branch where to publish commits') publish_parser.set_defaults(func=main) diff --git a/gitless/cli/gl_remote.py b/gitless/cli/gl_remote.py index 4957f97..bb549f9 100644 --- a/gitless/cli/gl_remote.py +++ b/gitless/cli/gl_remote.py @@ -5,8 +5,6 @@ """gl remote - List, create, edit or delete remotes.""" -from __future__ import unicode_literals - from . import pprint @@ -14,7 +12,7 @@ def parser(subparsers, _): """Adds the remote parser to the given subparsers object.""" desc = 'list, create, edit or delete remotes' remote_parser = subparsers.add_parser( - 'remote', help=desc, description=desc.capitalize()) + 'remote', help=desc, description=desc.capitalize(), aliases=['rt']) remote_parser.add_argument( '-c', '--create', nargs='?', help='create remote', dest='remote_name', metavar='remote') @@ -24,6 +22,11 @@ def parser(subparsers, _): remote_parser.add_argument( '-d', '--delete', nargs='+', help='delete remote(es)', dest='delete_r', metavar='remote') + remote_parser.add_argument( + '-rn', '--rename', nargs='+', + help='renames the specified remote: accepts two arguments ' + '(current remote name and new remote name)', + dest='rename_r') remote_parser.set_defaults(func=main) @@ -36,6 +39,8 @@ def main(args, repo): ret = _do_create(args.remote_name, args.remote_url, remotes) elif args.delete_r: ret = _do_delete(args.delete_r, remotes) + elif args.rename_r: + ret = _do_rename(args.rename_r, remotes) else: ret = _do_list(remotes) @@ -76,3 +81,20 @@ def _do_delete(delete_r, remotes): pprint.err('Remote \'{0}\' doesn\'t exist'.format(r)) errors_found = True return not errors_found + + +def _do_rename(rename_r, remotes): + errors_found = False + if len(rename_r) != 2: + pprint.err( + 'Expected 2 arguments in the folllowing format: ' + 'gl remote -rn current_remote_name new_remote_name') + errors_found = True + else: + try: + remotes.rename(rename_r[0], rename_r[1]) + pprint.ok('Renamed remote {0} to {1}'.format(rename_r[0], rename_r[1])) + except KeyError: + pprint.err('Remote \'{0}\' doesn\'t exist'.format(rename_r[0])) + errors_found = True + return not errors_found diff --git a/gitless/cli/gl_resolve.py b/gitless/cli/gl_resolve.py index 213df43..742182e 100644 --- a/gitless/cli/gl_resolve.py +++ b/gitless/cli/gl_resolve.py @@ -5,9 +5,7 @@ """gl resolve - Mark a file with conflicts as resolved.""" -from __future__ import unicode_literals - from . import file_cmd -parser = file_cmd.parser('mark files with conflicts as resolved', 'resolve') +parser = file_cmd.parser('mark files with conflicts as resolved', 'resolve', ['rs']) diff --git a/gitless/cli/gl_status.py b/gitless/cli/gl_status.py index 2524ef2..1be3398 100644 --- a/gitless/cli/gl_status.py +++ b/gitless/cli/gl_status.py @@ -5,12 +5,8 @@ """gl status - Show the status of files in the repo.""" -from __future__ import unicode_literals - import os -from clint.textui import colored - from gitless import core from . import helpers, pprint @@ -20,7 +16,7 @@ def parser(subparsers, repo): """Adds the status parser to the given subparsers object.""" desc = 'show status of the repo' status_parser = subparsers.add_parser( - 'status', help=desc, description=desc.capitalize()) + 'status', help=desc, description=desc.capitalize(), aliases=['st']) status_parser.add_argument( 'paths', nargs='*', help='the specific path(s) to status', action=helpers.PathProcessor, repo=repo) @@ -30,7 +26,7 @@ def parser(subparsers, repo): def main(args, repo): curr_b = repo.current_branch pprint.msg('On branch {0}, repo-directory {1}'.format( - colored.green(curr_b.branch_name), colored.green('//' + repo.cwd))) + pprint.green(curr_b.branch_name), pprint.green('//' + repo.cwd))) if curr_b.merge_in_progress: pprint.blank() @@ -83,16 +79,16 @@ def _print_tracked_mod_files(tracked_mod_list, relative_paths, repo): root = repo.root for f in tracked_mod_list: exp = '' - color = colored.yellow + color = pprint.yellow if not f.exists_at_head: exp = ' (new file)' - color = colored.green + color = pprint.green elif not f.exists_in_wd: exp = ' (deleted)' - color = colored.red + color = pprint.red elif f.in_conflict: exp = ' (with conflicts)' - color = colored.cyan + color = pprint.cyan fp = os.path.relpath(os.path.join(root, f.fp)) if relative_paths else f.fp if fp == '.': @@ -114,12 +110,12 @@ def _print_untracked_files(untracked_list, relative_paths, repo): root = repo.root for f in untracked_list: exp = '' - color = colored.blue + color = pprint.blue if f.in_conflict: exp = ' (with conflicts)' - color = colored.cyan + color = pprint.cyan elif f.exists_at_head: - color = colored.magenta + color = pprint.magenta if f.exists_in_wd: exp = ' (exists at head)' else: diff --git a/gitless/cli/gl_switch.py b/gitless/cli/gl_switch.py index a2ef0ce..01a09a7 100644 --- a/gitless/cli/gl_switch.py +++ b/gitless/cli/gl_switch.py @@ -5,8 +5,6 @@ """gl switch - Switch branches.""" -from __future__ import unicode_literals - from . import pprint @@ -14,13 +12,17 @@ def parser(subparsers, _): """Adds the switch parser to the given subparsers object.""" desc = 'switch branches' switch_parser = subparsers.add_parser( - 'switch', help=desc, description=desc.capitalize()) + 'switch', help=desc, description=desc.capitalize(), aliases=['sw']) switch_parser.add_argument('branch', help='switch to branch') switch_parser.add_argument( '-mo', '--move-over', help='move uncomitted changes made in the current branch to the ' 'destination branch', action='store_true') + switch_parser.add_argument('-mi', '--move-ignored', + help='move ignored files to the destination branch, ' + 'has no effect if --move-over is also set', + action='store_true') switch_parser.set_defaults(func=main) @@ -33,6 +35,6 @@ def main(args, repo): pprint.err_exp('to create a new branch do gl branch -c {0}'.format(args.branch)) return False - repo.switch_current_branch(b, move_over=args.move_over) + repo.switch_current_branch(b, move_over=args.move_over, move_ignored=args.move_ignored) pprint.ok('Switched to branch {0}'.format(args.branch)) return True diff --git a/gitless/cli/gl_tag.py b/gitless/cli/gl_tag.py index 4035caa..789a7b6 100644 --- a/gitless/cli/gl_tag.py +++ b/gitless/cli/gl_tag.py @@ -5,8 +5,6 @@ """gl tag - List, create, edit or delete tags.""" -from __future__ import unicode_literals - from gitless import core from . import helpers, pprint @@ -16,7 +14,7 @@ def parser(subparsers, _): """Adds the tag parser to the given subparsers object.""" desc = 'list, create, or delete tags' tag_parser = subparsers.add_parser( - 'tag', help=desc, description=desc.capitalize()) + 'tag', help=desc, description=desc.capitalize(), aliases=['tg']) list_group = tag_parser.add_argument_group('list tags') list_group.add_argument( diff --git a/gitless/cli/gl_track.py b/gitless/cli/gl_track.py index afcfd0d..c22b8f8 100644 --- a/gitless/cli/gl_track.py +++ b/gitless/cli/gl_track.py @@ -5,9 +5,7 @@ """gl track - Start tracking changes to files.""" -from __future__ import unicode_literals - from . import file_cmd -parser = file_cmd.parser('start tracking changes to files', 'track') +parser = file_cmd.parser('start tracking changes to files', 'track', ['tr']) diff --git a/gitless/cli/gl_untrack.py b/gitless/cli/gl_untrack.py index 2f2d2d7..670db33 100644 --- a/gitless/cli/gl_untrack.py +++ b/gitless/cli/gl_untrack.py @@ -5,9 +5,7 @@ """gl untrack - Stop tracking changes to files.""" -from __future__ import unicode_literals - from . import file_cmd -parser = file_cmd.parser('stop tracking changes to files', 'untrack') +parser = file_cmd.parser('stop tracking changes to files', 'untrack', ['un']) diff --git a/gitless/cli/helpers.py b/gitless/cli/helpers.py index dc7e34c..61efd63 100644 --- a/gitless/cli/helpers.py +++ b/gitless/cli/helpers.py @@ -5,8 +5,6 @@ """Some helpers for commands.""" -from __future__ import unicode_literals - import argparse import os import subprocess @@ -112,13 +110,21 @@ def __init__( def __call__(self, parser, namespace, paths, option_string=None): root = self.repo.root if self.repo else '' - repo_dir = self.repo.path[:-1] if self.repo else '' # strip trailing / + repo_path = self.repo.path if self.repo else '' + # We add the sep so that we can use `startswith` to determine if a file + # is inside the .git folder + # `normpath` is important because libgit2 returns the repo_path with forward + # slashes on Windows + normalized_repo_path = os.path.normpath(repo_path) + os.path.sep def process_paths(): for path in paths: path = os.path.abspath(path) - if self.recursive and os.path.isdir(path): + # Treat symlinks as normal files, even if the link points to a + # directory. The directory could be outside of the repo, then things + # get weird... This is standard git behavior. + if self.recursive and os.path.isdir(path) and not os.path.islink(path): for curr_dir, dirs, fps in os.walk(path, topdown=True): - if curr_dir.startswith(repo_dir): + if curr_dir.startswith(normalized_repo_path): dirs[:] = [] continue curr_dir_rel = os.path.relpath(curr_dir, root) @@ -129,12 +135,10 @@ def process_paths(): dirs[:] = [] continue for fp in fps: - yield os.path.join(curr_dir_rel, fp) + yield fp if curr_dir_rel == '.' else os.path.join(curr_dir_rel, fp) else: - if not path.startswith(repo_dir): + if not path.startswith(normalized_repo_path): yield os.path.relpath(path, root) - else: - yield path setattr(namespace, self.dest, process_paths()) @@ -248,3 +252,36 @@ def validate(fps, check_fn, msg): for e in err: pprint.err(e) return False + +"""Aliases for argparse positional arguments.""" + +class AliasedSubParsersAction(argparse._SubParsersAction): + + class _AliasedPseudoAction(argparse.Action): + def __init__(self, name, aliases, help): + dest = name + if aliases: + dest += ' (%s)' % ','.join(aliases) + sup = super(AliasedSubParsersAction._AliasedPseudoAction, self) + sup.__init__(option_strings=[], dest=dest, help=help) + + def add_parser(self, name, **kwargs): + if 'aliases' in kwargs: + aliases = kwargs['aliases'] + del kwargs['aliases'] + else: + aliases = [] + + parser = super(AliasedSubParsersAction, self).add_parser(name, **kwargs) + + # Make the aliases work. + for alias in aliases: + self._name_parser_map[alias] = parser + # Make the help text reflect them, first removing old help entry. + if 'help' in kwargs: + help = kwargs.pop('help') + self._choices_actions.pop() + pseudo_action = self._AliasedPseudoAction(name, aliases, help) + self._choices_actions.append(pseudo_action) + + return parser diff --git a/gitless/cli/pprint.py b/gitless/cli/pprint.py index 8faa349..1c84766 100644 --- a/gitless/cli/pprint.py +++ b/gitless/cli/pprint.py @@ -5,8 +5,6 @@ """Module for pretty printing Gitless output.""" -from __future__ import unicode_literals - try: from StringIO import StringIO except ImportError: @@ -17,8 +15,7 @@ import re import sys -from clint.textui import colored, indent -from clint.textui import puts as clint_puts +DISABLE_COLOR = False from gitless import core @@ -28,28 +25,62 @@ '######') -IS_PY2 = sys.version_info[0] == 2 ENCODING = getpreferredencoding() or 'utf-8' def puts(s='', newline=True, stream=sys.stdout.write): - assert not IS_PY2 or ( - isinstance(s, unicode) or isinstance(s, colored.ColoredString)) + if newline: + s = s + '\n' + stream(s) + + +# Colored strings +RED = '\033[31m' +RED_BOLD = '\033[1;31m' +GREEN = '\033[32m' +GREEN_BOLD = '\033[1;32m' +YELLOW = '\033[33m' +BLUE = '\033[34m' +MAGENTA = '\033[35m' +CYAN = '\033[36m' +CLEAR = '\033[0m' + +def _color(color_code, text): + return '{0}{1}{2}'.format(color_code, text, CLEAR) if should_color() else text + +def should_color(): + # We only output colored lines if the coloring is enabled and we are not being + # piped or redirected + return not DISABLE_COLOR and sys.stdout.isatty() + +def red(text): + return _color(RED, text) + +def green(text): + return _color(GREEN, text) + +def yellow(text): + return _color(YELLOW, text) + +def blue(text): + return _color(BLUE, text) + +def magenta(text): + return _color(MAGENTA, text) - if IS_PY2: - s = s.encode(ENCODING, errors='ignore') - clint_puts(s, newline=newline, stream=stream) +def cyan(text): + return _color(CYAN, text) # Stdout def ok(text): - puts(colored.green('✔ {0}'.format(text))) + puts(green('✔ {0}'.format(text))) def warn(text): - puts(colored.yellow('! {0}'.format(text))) + puts(yellow('! {0}'.format(text))) def msg(text, stream=sys.stdout.write): @@ -57,13 +88,11 @@ def msg(text, stream=sys.stdout.write): def exp(text, stream=sys.stdout.write): - with indent(2): - puts('➜ {0}'.format(text), stream=stream) + puts(' ➜ {0}'.format(text), stream=stream) def item(i, opt_text='', stream=sys.stdout.write): - with indent(4): - puts('{0}{1}'.format(i, opt_text), stream=stream) + puts(' {0}{1}'.format(i, opt_text), stream=stream) def blank(stream=sys.stdout.write): @@ -77,7 +106,7 @@ def sep(stream=sys.stdout.write): # Err def err(text): - puts(colored.red('✘ {0}'.format(text)), stream=sys.stderr.write) + puts(red('✘ {0}'.format(text)), stream=sys.stderr.write) def err_msg(text): @@ -135,9 +164,9 @@ def commit_str(ci): return ci_str.getvalue().strip() -def commit(ci, compact=False, stream=sys.stdout.write): +def commit(ci, compact=False, stream=sys.stdout.write, line_additions=0, line_deletions=0): merge_commit = len(ci.parent_ids) > 1 - color = colored.magenta if merge_commit else colored.yellow + color = magenta if merge_commit else yellow if compact: title = ci.message.splitlines()[0] puts('{0} {1}'.format(color(str(ci.id)[:7]), title), stream=stream) @@ -152,9 +181,12 @@ def commit(ci, compact=False, stream=sys.stdout.write): ci_author_dt = datetime.fromtimestamp( ci.author.time, FixedOffset(ci.author.offset)) puts(color('Date: {0:%c %z}'.format(ci_author_dt)), stream=stream) + put_s = lambda num: '' if num == 1 else 's' + puts(color('Stats: {0} line{1} added, {2} line{3} removed' + .format(line_additions, put_s(line_additions), + line_deletions, put_s(line_deletions))), stream=stream) puts(stream=stream) - with indent(4): - puts(ci.message, stream=stream) + puts(' {0}'.format(ci.message), stream=stream) # Op Callbacks @@ -199,7 +231,7 @@ def diff(patch, stream=sys.stdout.write): new_fp = patch.delta.new_file.path puts('Diff of file "{0}"'.format(old_fp), stream=stream) if old_fp != new_fp: - puts(colored.cyan(' (renamed to {0})'.format(new_fp)), stream=stream) + puts(cyan(' (renamed to {0})'.format(new_fp)), stream=stream) puts(stream=stream) if patch.delta.is_binary: @@ -226,16 +258,25 @@ def diff(patch, stream=sys.stdout.write): puts(stream=stream) puts(stream=stream) +def diff_totals(total_additions, total_deletions, stream=sys.stdout.write): + + put_s = lambda num: '' if num == 1 else 's' + puts('Diff summary', stream=stream) + puts('Total of {0} line{1} added' + .format(total_additions, put_s(total_additions)), stream=stream) + puts('Total of {0} line{1} removed' + .format(total_deletions, put_s(total_deletions)), stream=stream) + puts(stream=stream) + def _hunk(hunk, stream=sys.stdout.write): - puts(colored.cyan('@@ -{0},{1} +{2},{3} @@'.format( + puts(cyan('@@ -{0},{1} +{2},{3} @@'.format( hunk.old_start, hunk.old_lines, hunk.new_start, hunk.new_lines)), stream=stream) padding = _padding(hunk) del_line, add_line, maybe_bold, saw_add = None, None, False, False for diff_line in hunk.lines: - assert not IS_PY2 or isinstance(diff_line.content, unicode) st = diff_line.origin if st == '-' and not maybe_bold: @@ -291,21 +332,18 @@ def _format_line(diff_line, padding, bold_delim=None): Returns: a padded and colored version of the diff line with line numbers """ - # Color constants - # We only output colored lines if the coloring is enabled and we are not being - # piped or redirected - if colored.DISABLE_COLOR or not sys.stdout.isatty(): - GREEN = '' - GREEN_BOLD = '' - RED = '' - RED_BOLD = '' - CLEAR = '' + if should_color(): + green = GREEN + green_bold = GREEN_BOLD + red = RED + red_bold = RED_BOLD + clear = CLEAR else: - GREEN = '\033[32m' - GREEN_BOLD = '\033[1;32m' - RED = '\033[31m' - RED_BOLD = '\033[1;31m' - CLEAR = '\033[0m' + green = '' + green_bold = '' + red = '' + red_bold = '' + clear = '' formatted = '' st = diff_line.origin @@ -317,25 +355,25 @@ def _format_line(diff_line, padding, bold_delim=None): formatted = ( str(old_lineno).ljust(padding) + str(new_lineno).ljust(padding) + line) elif st == '+': - formatted = ' ' * padding + GREEN + str(new_lineno).ljust(padding) + formatted = ' ' * padding + green + str(new_lineno).ljust(padding) if not bold_delim: formatted += line else: bold_start, bold_end = bold_delim formatted += ( - line[:bold_start] + GREEN_BOLD + line[bold_start:bold_end] + CLEAR + - GREEN + line[bold_end:]) + line[:bold_start] + green_bold + line[bold_start:bold_end] + clear + + green + line[bold_end:]) elif st == '-': - formatted = RED + str(old_lineno).ljust(padding) + ' ' * padding + formatted = red + str(old_lineno).ljust(padding) + ' ' * padding if not bold_delim: formatted += line else: bold_start, bold_end = bold_delim formatted += ( - line[:bold_start] + RED_BOLD + line[bold_start:bold_end] + CLEAR + - RED + line[bold_end:]) + line[:bold_start] + red_bold + line[bold_start:bold_end] + clear + + red + line[bold_end:]) - return formatted + CLEAR + return formatted + clear def _highlight(line1, line2): diff --git a/gitless/core.py b/gitless/core.py index a6a1780..258d887 100644 --- a/gitless/core.py +++ b/gitless/core.py @@ -5,15 +5,9 @@ """Gitless's library.""" -from __future__ import unicode_literals - import collections import errno import io -try: - from itertools import izip as zip -except ImportError: - pass import itertools import json @@ -21,19 +15,11 @@ import os import re import shutil - -import pygit2 - - import sys -if sys.platform != 'win32': - from sh import git, ErrorReturnCode -else: - from pbs import Command, ErrorReturnCode - git = Command('git') -git = git.bake('--no-pager') +import pygit2 +from subprocess import run, CalledProcessError ENCODING = getpreferredencoding() or 'utf-8' @@ -65,12 +51,16 @@ def error_on_none(path): return path -def init_repository(url=None): +def init_repository(url=None, only=None, exclude=None): """Creates a new Gitless's repository in the cwd. Args: url: if given the local repository will be a clone of the remote repository given by this url. + only: if given, this local repository will consist only of the branches + in this set + exclude: if given, and only is not given, this local repository will + consistent of all branches not in this set """ cwd = os.getcwd() try: @@ -80,20 +70,25 @@ def init_repository(url=None): if not url: repo = pygit2.init_repository(cwd) # We also create an initial root commit - git.commit(allow_empty=True, m='Initialize repository') + git('commit', '--allow-empty', '-m', 'Initialize repository') return repo try: - git.clone(url, cwd) - except ErrorReturnCode as e: - raise GlError(stderr(e)) + git('clone', url, cwd) + except CalledProcessError as e: + raise GlError(e.stderr) # We get all remote branches as well and create local equivalents + # Flags: only branches take precedence over exclude branches. repo = Repository() remote = repo.remotes['origin'] for rb in (remote.lookup_branch(bn) for bn in remote.listall_branches()): if rb.branch_name == 'master': continue + if only and rb.branch_name not in only: + continue + elif not only and exclude and rb.branch_name in exclude: + continue new_b = repo.create_branch(rb.branch_name, rb.head) new_b.upstream = rb return repo @@ -137,6 +132,8 @@ def revparse_single(self, revision): return self.remotes[remote].lookup_branch(remote_branch).head except KeyError: pass + except AttributeError: + pass try: return self.git_repo.revparse_single(revision) except (KeyError, ValueError): @@ -245,13 +242,15 @@ def listall_branches(self): """ return self.git_repo.listall_branches(pygit2.GIT_BRANCH_LOCAL) - def switch_current_branch(self, dst_b, move_over=False): + def switch_current_branch(self, dst_b, move_over=False, move_ignored=False): """Switches to the given branch. Args: dst_b: the destination branch. move_over: if True, then uncommitted changes made in the current branch are moved to the destination branch (defaults to False). + move_ignored: if True, and move_over is False, then ignored files are moved + to the destination branch, but uncommitted changes are not (defaults to False). """ if dst_b.is_current: raise ValueError( @@ -269,17 +268,16 @@ def switch_current_branch(self, dst_b, move_over=False): git_repo = self.git_repo au_fp = lambda b: os.path.join( self.path, 'GL_AU_{0}'.format(b.branch_name.replace('/', '_'))) - update_index = git.bake('update-index', _cwd=self.root) def save(b): msg = _stash_msg(b.branch_name) # Save assumed unchanged info - au_fps = ' '.join(b._au_files()) + au_fps = list(b._au_files()) if au_fps: with io.open(au_fp(b), mode='w', encoding=ENCODING) as f: - f.write(au_fps) - update_index('--no-assume-unchanged', au_fps) + f.write(' '.join(au_fps)) + git('update-index', '--no-assume-unchanged', *au_fps, cwd=self.root) if b.merge_in_progress or b.fuse_in_progress: body = {} @@ -335,7 +333,10 @@ def save(b): if not move_over: # Stash - git.stash.save('--all', '--', msg) + if move_ignored: + git('stash', 'save', '--include-untracked', '--', msg) + else: + git('stash', 'save', '--all', '--', msg) def restore(b): s_id, msg = _stash(_stash_msg(b.branch_name)) @@ -347,14 +348,14 @@ def restore_au_info(): if os.path.exists(au): with io.open(au, mode='r', encoding=ENCODING) as f: au_fps = f.read() - update_index('--assume-unchanged', au_fps) + git('update-index', '--assume-unchanged', *au_fps.split()) os.remove(au) split_msg = msg.split(INFO_SEP) if len(split_msg) == 1: # No op to restore # Pop - git.stash.pop(s_id) + git('stash', 'pop', s_id) # Restore assumed unchanged info restore_au_info() else: # Restore op @@ -371,7 +372,7 @@ def restore_au_info(): self._ref_create('MERGE_HEAD', ref_info['MERGE_HEAD']) # Pop - git.stash.pop(s_id) + git('stash', 'pop', s_id) # Restore conflict info conf_info = body[CONF_INFO] @@ -388,8 +389,8 @@ def restore_au_info(): if index_e[THEIRS]: index_info.append(build_entry(index_e[THEIRS], 3)) - update_index('--unresolve', _in=' '.join(conf_info.keys())) - update_index('--index-info', _in='\n'.join(index_info)) + git('update-index', '--unresolve', _in=' '.join(conf_info.keys())) + git('update-index', '--index-info', _in='\n'.join(index_info)) # Restore msg info merge_msg_fp = os.path.join(self.path, 'MERGE_MSG') @@ -436,14 +437,17 @@ def create(self, name, url): # Check that the given url corresponds to a git repo try: git('ls-remote', '--heads', url) - except ErrorReturnCode as e: - raise ValueError(stderr(e)) + except CalledProcessError as e: + raise ValueError(e.stderr) self.git_remote_collection.create(name, url) def delete(self, name): self.git_remote_collection.delete(name) + def rename(self, name, new_name): + self.git_remote_collection.rename(name, new_name) + class Remote(object): """Tracked remote repository. @@ -472,10 +476,10 @@ def create_branch(self, name, head): # push tmp_b = self.gl_repo.create_branch('gl_tmp_ref', head) try: - git.push(self.name, '{0}:{1}'.format(tmp_b, name)) + git('push', self.name, '{0}:{1}'.format(tmp_b, name)) return self.lookup_branch(name) - except ErrorReturnCode as e: - raise GlError(stderr(e)) + except CalledProcessError as e: + raise GlError(e.stderr) finally: tmp_b.delete() @@ -486,18 +490,33 @@ def listall_branches(self): to each name. """ regex = re.compile(r'.*\trefs/heads/(.*)') - for head in stdout(git('ls-remote', '--heads', self.name)).splitlines(): + for head in git('ls-remote', '--heads', self.name).splitlines(): yield regex.match(head).group(1) def lookup_branch(self, branch_name): - if not stdout(git('ls-remote', '--heads', self.name, branch_name)): - return None - # The branch exists in the remote - git.fetch(self.git_remote.name, branch_name) - git_branch = self.gl_repo.git_repo.lookup_branch( - self.git_remote.name + '/' + branch_name, pygit2.GIT_BRANCH_REMOTE) - return RemoteBranch(git_branch, self.gl_repo) + branches = self.lookup_branches([branch_name]) + return branches[0] if branches else None + def lookup_branches(self, branch_names): + try: + git('fetch', self.git_remote.name, *branch_names) + except: + return None + remote_branches = [] + for branch_name in branch_names: + git_branch = self.gl_repo.git_repo.lookup_branch( + self.git_remote.name + '/' + branch_name, pygit2.GIT_BRANCH_REMOTE) + # Make another check for the branch being None + # As observed in issue : https://github.com/sdg-mit/gitless/issues/211 + if git_branch is None: + git('fetch', self.git_remote.name) + git_branch = self.gl_repo.git_repo.lookup_branch( + self.git_remote.name + '/' + branch_name, pygit2.GIT_BRANCH_REMOTE) + remote_branches.append(RemoteBranch(git_branch, self.gl_repo)) + return remote_branches + + def lookupall_branches(self): + return self.lookup_branches(sorted(self.listall_branches())) # Tag-related methods @@ -511,10 +530,10 @@ def create_tag(self, name, commit): # push tmp_t = self.gl_repo.create_tag('gl_tmp_ref', commit) try: - git.push(self.name, 'refs/tags/{0}:refs/tags/{1}'.format(tmp_t, name)) + git('push', self.name, 'refs/tags/{0}:refs/tags/{1}'.format(tmp_t, name)) return self.lookup_tag(name) - except ErrorReturnCode as e: - raise GlError(stderr(e)) + except CalledProcessError as e: + raise GlError(e.stderr) finally: tmp_t.delete() @@ -525,18 +544,18 @@ def listall_tags(self): to each name. """ regex = re.compile(r'.*\trefs/tags/(.*)') - for head in stdout(git('ls-remote', '--tags', self.name)).splitlines(): + for head in git('ls-remote', '--tags', self.name).splitlines(): tag_name = regex.match(head).group(1) if tag_name.endswith('^{}'): continue yield tag_name def lookup_tag(self, tag_name): - tag_info = stdout(git('ls-remote', '--tags', self.name, tag_name)) + tag_info = git('ls-remote', '--tags', self.name, tag_name) if not tag_info: return None # The tag exists in the remote - git.fetch(self.git_remote.name, tag_name) + git('fetch', self.git_remote.name, tag_name) regex = re.compile(r'(.*)\trefs/tags/.*') commit_id = regex.match(tag_info).group(1) @@ -562,9 +581,9 @@ def __init__(self, remote_name, tag_name, commit): def delete(self): try: - git.push(self.remote_name, ':{0}'.format(self.tag_name)) - except ErrorReturnCode as e: - raise GlError(stderr(e)) + git('push', self.remote_name, ':{0}'.format(self.tag_name)) + except CalledProcessError as e: + raise GlError(e.stderr) def __str__(self): return self.remote_name + '/' + self.tag_name @@ -588,9 +607,9 @@ def __init__(self, git_branch, gl_repo): def delete(self): try: - git.push(self.remote_name, ':{0}'.format(self.branch_name)) - except ErrorReturnCode as e: - raise GlError(stderr(e)) + git('push', self.remote_name, ':{0}'.format(self.branch_name)) + except CalledProcessError as e: + raise GlError(e.stderr) @property def target(self): @@ -607,7 +626,7 @@ def history(self, reverse=False): return walker(self.gl_repo.git_repo, self.target, reverse=reverse) def _update(self): - git.fetch(self.remote_name, self.branch_name) + git('fetch', self.remote_name, self.branch_name) self.git_branch = self.gl_repo.git_repo.lookup_branch( self.remote_name + '/' + self.branch_name, pygit2.GIT_BRANCH_REMOTE) @@ -642,7 +661,10 @@ def delete(self): # We also cleanup any stash left s_id, _ = _stash(_stash_msg(self.branch_name)) if s_id: - git.stash.drop(s_id) + git('stash', 'drop', s_id) + + def rename(self, new_name): + self.git_branch.rename(new_name) @property def upstream(self): @@ -757,8 +779,7 @@ def __getattr__(self, name): 'in_conflict']) def _au_files(self): - for f_out in stdout( - git('ls-files', '-v', _cwd=self.gl_repo.root)).splitlines(): + for f_out in git('ls-files', '-v', cwd=self.gl_repo.root).splitlines(): if f_out[0] == 'h': yield f_out[2:].strip() @@ -772,7 +793,7 @@ def status(self): yield self.FileStatus(fp, *self._st_map[git_s]) # status doesn't report au files - au_files = self._au_files() + au_files = list(self._au_files()) if au_files: for fp in au_files: exists_in_wd = os.path.exists(os.path.join(self.gl_repo.root, fp)) @@ -788,7 +809,7 @@ def _status_file(self, path): git_st = self.gl_repo.git_repo.status_file(_get_git_path(path)) root = self.gl_repo.root - cmd_out = stdout(git('ls-files', '-v', '--full-name', path, _cwd=root)) + cmd_out = git('ls-files', '-v', '--full-name', path, cwd=root) is_au = cmd_out and cmd_out[0] == 'h' if is_au: exists_in_wd = os.path.exists(os.path.join(root, path)) @@ -829,8 +850,7 @@ def track_file(self, path): git_path = _get_git_path(path) index.add(git_path) elif is_au: # Case (ii) - git('update-index', '--no-assume-unchanged', path, - _cwd=self.gl_repo.root) + git('update-index', '--no-assume-unchanged', path, cwd=self.gl_repo.root) else: raise GlError('File {0} in unknown status {1}'.format(path, git_st)) @@ -860,8 +880,7 @@ def untrack_file(self, path): git_path = _get_git_path(path) index.remove(git_path) elif not is_au: # Case (ii) - git('update-index', '--assume-unchanged', path, - _cwd=self.gl_repo.root) + git('update-index', '--assume-unchanged', path, cwd=self.gl_repo.root) else: raise GlError('File {0} in unknown status {1}'.format(path, git_st)) @@ -963,18 +982,18 @@ def merge(self, src, op_cb=None): if result & pygit2.GIT_MERGE_ANALYSIS_UP_TO_DATE: raise GlError('No commits to merge') try: - git.merge(src, '--no-ff') - except ErrorReturnCode as e: - err = stderr(e) + git('merge', str(src), '--no-ff') + except CalledProcessError as e: + err = e.stderr if not 'stash' in err: - raise GlError(stdout(e) + err) + raise GlError(e.stdout + err) if op_cb and op_cb.save: op_cb.save() - git.stash.save('--', _stash_msg_merge(self)) + git('stash', 'save', '--', _stash_msg_merge(self)) try: - git.merge(src, '--no-ff') - except ErrorReturnCode as e: - raise GlError(stdout(e) + stderr(e)) + git('merge', str(src), '--no-ff') + except CalledProcessError as e: + raise GlError(e.stdout + e.stderr) self._state_cleanup() restore_fn = op_cb.restore_ok if op_cb else None @@ -994,7 +1013,7 @@ def merge_in_progress(self): def abort_merge(self): if not self.merge_in_progress: raise GlError('No merge in progress, nothing to abort') - git.merge(abort=True) + git('merge', '--abort') # Fuse-related methods @@ -1160,7 +1179,7 @@ def _safe_reset(self, cid, msg_fn, save_fn=None): # case either so we need to find an alternative way of doing this) if save_fn: save_fn() - git.stash.save('--', msg_fn(self)) + git('stash', 'save', '--', msg_fn(self)) git_repo.checkout_tree(tree) git_repo.reset(cid, pygit2.GIT_RESET_SOFT) @@ -1168,10 +1187,10 @@ def _safe_restore(self, msg_fn, restore_fn=None): s_id, _ = _stash(msg_fn(self)) if s_id: try: - git.stash.pop(s_id) + git('stash', 'pop', s_id) if restore_fn: restore_fn() - except ErrorReturnCode: + except CalledProcessError: raise ApplyFailedError( 'Uncommitted changes failed to apply onto the new head of the ' 'branch') @@ -1265,13 +1284,14 @@ def publish(self, branch): assert branch.branch_name in self.gl_repo.remotes[ branch.remote_name].listall_branches() - cmd = git.push( - branch.remote_name, - '{0}:{1}'.format(self.branch_name, branch.branch_name)) - if 'Everything up-to-date' in stderr(cmd): + cmd = git_p( + 'push', + branch.remote_name, + '{0}:{1}'.format(self.branch_name, branch.branch_name)) + if 'Everything up-to-date' in cmd.stderr: raise GlError('No commits to publish') - except ErrorReturnCode as e: - err_msg = stderr(e) + except CalledProcessError as e: + err_msg = e.stderr if 'Updates were rejected' in err_msg: raise GlError('There are changes you need to fuse/merge') raise GlError(err_msg) @@ -1304,7 +1324,7 @@ def __init__(self, tag_name, commit): self.commit = commit def delete(self): - git.tag('-d', self.tag_name) + git('tag', '-d', self.tag_name) def __str__(self): return self.tag_name @@ -1314,18 +1334,18 @@ def __str__(self): def _stash(pattern): """Returns the id and msg of the stash that matches the given pattern.""" - out = stdout(git.stash.list(grep=pattern, format='|*|%gd|*|%B|*|')) + out = git('stash', 'list', '--grep', pattern, '--format=|%gd|%s|') if not out: return None, None - result = re.match(r'\|\*\|(stash@\{.+\})\|\*\|(.*)\|\*\|', out, re.DOTALL) + result = re.match(r'\|(stash@\{.+\})\|(.*)\|', out, re.DOTALL) if not result: raise GlError('Unexpected output of git stash: {0}'.format(out)) return result.group(1).strip(), result.group(2).strip() def _stash_msg(name): - return '---gl-{0}---'.format(name) + return 'gl-{0}'.format(name) def _stash_msg_fuse(name): return _stash_msg('fuse-{0}'.format(name)) @@ -1339,19 +1359,14 @@ def _stash_msg_merge(name): OpCb = collections.namedtuple( 'OpCb', ['apply_ok', 'apply_err', 'save', 'restore_ok']) -def stdout(p): - try: - pstdout = p.stdout.decode(ENCODING) - except AttributeError: - pstdout = p.stdout - return pstdout +def git(*args, cwd=None, _in=None): + return git_p(*args, cwd=cwd, _in=_in).stdout -def stderr(p): - try: - pstderr = p.stderr.decode(ENCODING) - except AttributeError: - pstderr = p.stderr - return pstderr +def git_p(*args, cwd=None, _in=None): + p = run( + ['git', '--no-pager', *args], check=True, capture_output=True, cwd=cwd, + input=_in, encoding=ENCODING) + return p def walker(git_repo, target, reverse): flags = pygit2.GIT_SORT_TOPOLOGICAL | pygit2.GIT_SORT_TIME diff --git a/gitless/tests/test_core.py b/gitless/tests/test_core.py index 8037446..8002018 100644 --- a/gitless/tests/test_core.py +++ b/gitless/tests/test_core.py @@ -5,21 +5,17 @@ """Core unit tests.""" -from __future__ import unicode_literals - from functools import wraps import os import shutil import tempfile - +import unittest +import argparse import sys -if sys.platform != 'win32': - from sh import git -else: - from pbs import Command - git = Command('git') +from subprocess import CalledProcessError from gitless import core +from gitless.cli import gl, helpers, gl_track import gitless.tests.utils as utils_lib @@ -34,7 +30,18 @@ IGNORED_FP_WITH_SPACE = 'f3 space' NONEXISTENT_FP = 'nonexistent' NONEXISTENT_FP_WITH_SPACE = 'nonexistent space' +GITIGNORE_FP = '.gitignore' DIR = 'dir' +REPO_DIR = '.git' +REPO_FP = os.path.join(REPO_DIR, 'HEAD') +GITTEST_DIR = '.gittest' +GITTEST_FP = os.path.join(GITTEST_DIR, 'fp') +SYMLINK_TARGET = 'symtarget' +SYMLINK_TARGET_FP_CONTENTS = 'symf1\n' +SYMLINK_TARGET_FP = os.path.join(SYMLINK_TARGET, 'symf1') +SYMLINK_DIR = 'symdir' +SYMLINK_FP = os.path.join(SYMLINK_DIR, 'sym') +SYMLINK_GIT = 'gitsym' UNTRACKED_DIR_FP = os.path.join(DIR, 'f1') UNTRACKED_DIR_FP_WITH_SPACE = os.path.join(DIR, 'f1 space') TRACKED_DIR_FP = os.path.join(DIR, 'f2') @@ -49,12 +56,16 @@ IGNORED_FP, IGNORED_FP_WITH_SPACE, UNTRACKED_DIR_FP, UNTRACKED_DIR_FP_WITH_SPACE, TRACKED_DIR_FP, TRACKED_DIR_FP_WITH_SPACE, UNTRACKED_DIR_DIR_FP, UNTRACKED_DIR_DIR_FP_WITH_SPACE, TRACKED_DIR_DIR_FP, - TRACKED_DIR_DIR_FP_WITH_SPACE, '.gitignore'] + TRACKED_DIR_DIR_FP_WITH_SPACE, GITIGNORE_FP, GITTEST_FP, SYMLINK_TARGET_FP, + SYMLINK_FP, SYMLINK_GIT] +# the symbolic link is both a file and directory. The OS typically treats it +# like a directory but we want to treat it as a file for tracking purposes. ALL_DIR_FPS_IN_WD = [ TRACKED_DIR_FP, TRACKED_DIR_FP_WITH_SPACE, UNTRACKED_DIR_FP, UNTRACKED_DIR_FP_WITH_SPACE, TRACKED_DIR_DIR_FP, TRACKED_DIR_DIR_FP_WITH_SPACE, UNTRACKED_DIR_DIR_FP, - UNTRACKED_DIR_DIR_FP_WITH_SPACE] + UNTRACKED_DIR_DIR_FP_WITH_SPACE, GITTEST_DIR, SYMLINK_TARGET, SYMLINK_DIR, + SYMLINK_FP, SYMLINK_GIT] BRANCH = 'b1' REMOTE_BRANCH = 'rb' FP_IN_CONFLICT = 'f_conflict' @@ -68,7 +79,7 @@ class TestCore(utils_lib.TestBase): def setUp(self): super(TestCore, self).setUp('gl-core-test') - git.init() + utils_lib.git('init') utils_lib.set_test_config() self.repo = core.Repository() @@ -137,14 +148,16 @@ def setUp(self): utils_lib.write_file(TRACKED_DIR_DIR_FP, contents=TRACKED_FP_CONTENTS_1) utils_lib.write_file( TRACKED_DIR_DIR_FP_WITH_SPACE, contents=TRACKED_FP_CONTENTS_1) - git.add( - TRACKED_FP, TRACKED_FP_WITH_SPACE, - TRACKED_DIR_FP, TRACKED_DIR_FP_WITH_SPACE, - TRACKED_DIR_DIR_FP, TRACKED_DIR_DIR_FP_WITH_SPACE) - git.commit( - TRACKED_FP, TRACKED_FP_WITH_SPACE, - TRACKED_DIR_FP, TRACKED_DIR_FP_WITH_SPACE, - TRACKED_DIR_DIR_FP, TRACKED_DIR_DIR_FP_WITH_SPACE, m='1') + utils_lib.git( + 'add', + TRACKED_FP, TRACKED_FP_WITH_SPACE, + TRACKED_DIR_FP, TRACKED_DIR_FP_WITH_SPACE, + TRACKED_DIR_DIR_FP, TRACKED_DIR_DIR_FP_WITH_SPACE) + utils_lib.git( + 'commit', + TRACKED_FP, TRACKED_FP_WITH_SPACE, + TRACKED_DIR_FP, TRACKED_DIR_FP_WITH_SPACE, + TRACKED_DIR_DIR_FP, TRACKED_DIR_DIR_FP_WITH_SPACE, '-m', '1') utils_lib.write_file(TRACKED_FP, contents=TRACKED_FP_CONTENTS_2) utils_lib.write_file(TRACKED_FP_WITH_SPACE, contents=TRACKED_FP_CONTENTS_2) utils_lib.write_file(TRACKED_DIR_FP, contents=TRACKED_FP_CONTENTS_2) @@ -153,10 +166,11 @@ def setUp(self): utils_lib.write_file(TRACKED_DIR_DIR_FP, contents=TRACKED_FP_CONTENTS_2) utils_lib.write_file( TRACKED_DIR_DIR_FP_WITH_SPACE, contents=TRACKED_FP_CONTENTS_2) - git.commit( - TRACKED_FP, TRACKED_FP_WITH_SPACE, - TRACKED_DIR_FP, TRACKED_DIR_FP_WITH_SPACE, - TRACKED_DIR_DIR_FP, TRACKED_DIR_DIR_FP_WITH_SPACE, m='2') + utils_lib.git( + 'commit', + TRACKED_FP, TRACKED_FP_WITH_SPACE, + TRACKED_DIR_FP, TRACKED_DIR_FP_WITH_SPACE, + TRACKED_DIR_DIR_FP, TRACKED_DIR_DIR_FP_WITH_SPACE, '-m', '2') utils_lib.write_file(UNTRACKED_FP) utils_lib.write_file(UNTRACKED_FP_WITH_SPACE) utils_lib.write_file(UNTRACKED_DIR_FP) @@ -164,10 +178,17 @@ def setUp(self): utils_lib.write_file(UNTRACKED_DIR_DIR_FP) utils_lib.write_file(UNTRACKED_DIR_DIR_FP_WITH_SPACE) utils_lib.write_file( - '.gitignore', contents='{0}\n{1}'.format( + GITIGNORE_FP, contents='{0}\n{1}'.format( IGNORED_FP, IGNORED_FP_WITH_SPACE)) utils_lib.write_file(IGNORED_FP) utils_lib.write_file(IGNORED_FP_WITH_SPACE) + utils_lib.write_file(GITTEST_FP) + + # Testing with symlinks! The symlink calls will be no ops on Windows + utils_lib.write_file(SYMLINK_TARGET_FP, contents=SYMLINK_TARGET_FP_CONTENTS) + utils_lib.symlink(REPO_DIR, SYMLINK_GIT) + os.mkdir(SYMLINK_DIR) + utils_lib.symlink(SYMLINK_TARGET, SYMLINK_FP) self.curr_b = self.repo.current_branch @@ -259,6 +280,10 @@ def __assert_track_ignored(self, *fps): def test_track_ignored(self): self.__assert_track_ignored(IGNORED_FP, IGNORED_FP_WITH_SPACE) + @assert_contents_unchanged(GITIGNORE_FP) + def test_track_gitignore(self): + self.__assert_track_untracked(GITIGNORE_FP) + class TestFileUntrack(TestFile): @@ -709,17 +734,23 @@ def setUp(self): super(TestFileResolve, self).setUp() # Generate a conflict - git.checkout(b='branch') - utils_lib.write_file(FP_IN_CONFLICT, contents='branch') - utils_lib.write_file(DIR_FP_IN_CONFLICT, contents='branch') - git.add(FP_IN_CONFLICT, DIR_FP_IN_CONFLICT) - git.commit(FP_IN_CONFLICT, DIR_FP_IN_CONFLICT, m='branch') - git.checkout('master') + bname = 'branch' + utils_lib.git('checkout', '-b', bname) + utils_lib.write_file(FP_IN_CONFLICT, contents=bname) + utils_lib.write_file(DIR_FP_IN_CONFLICT, contents=bname) + utils_lib.git('add', FP_IN_CONFLICT, DIR_FP_IN_CONFLICT) + utils_lib.git('commit', FP_IN_CONFLICT, DIR_FP_IN_CONFLICT, '-m', bname) + utils_lib.git('checkout', 'master') utils_lib.write_file(FP_IN_CONFLICT, contents='master') utils_lib.write_file(DIR_FP_IN_CONFLICT, contents='master') - git.add(FP_IN_CONFLICT, DIR_FP_IN_CONFLICT) - git.commit(FP_IN_CONFLICT, DIR_FP_IN_CONFLICT, m='master') - git.merge('branch', _ok_code=[1]) + utils_lib.git('add', FP_IN_CONFLICT, DIR_FP_IN_CONFLICT) + utils_lib.git('commit', FP_IN_CONFLICT, DIR_FP_IN_CONFLICT, '-m', 'master') + try: + utils_lib.git('merge', bname) + raise Exception('The merge should have failed') + except CalledProcessError as e: + # we expect the merge to fail + pass @assert_no_side_effects(TRACKED_FP) def test_resolve_fp_with_no_conflicts(self): @@ -745,6 +776,75 @@ def test_resolve_relative(self): ValueError, 'no conflicts', self.curr_b.resolve_file, DIR_FP_IN_CONFLICT) +class TestFilePathProcessor(TestFile): + + def setUp(self): + super(TestFilePathProcessor, self).setUp() + self.parser = gl.build_parser([gl_track], self.repo) + + def test_path_processor_track_git(self): + argv = ['track', REPO_DIR] + args = self.parser.parse_args(argv) + files = [fp for fp in args.files] + # Should be empty in this case + self.assertFalse(files) + + @assert_contents_unchanged(REPO_FP) + def test_path_processor_track_git_file(self): + argv = ['track', REPO_FP] + args = self.parser.parse_args(argv) + files = [fp for fp in args.files] + self.assertFalse(files) + + @assert_no_side_effects(GITIGNORE_FP) + def test_path_processor_track_gitignore(self): + argv = ['track', GITIGNORE_FP] + args = self.parser.parse_args(argv) + files = [fp for fp in args.files] + + self.assertEqual(len(files), 1) + self.assertTrue(GITIGNORE_FP in files) + + @assert_no_side_effects(GITTEST_FP) + def test_path_processor_track_gittest_dir(self): + argv = ['track', GITTEST_DIR] + args = self.parser.parse_args(argv) + files = [fp for fp in args.files] + + self.assertEqual(len(files), 1) + self.assertTrue(GITTEST_FP in files) + + @assert_no_side_effects(GITTEST_FP) + def test_path_processor_track_gittest_fp(self): + argv = ['track', GITTEST_FP] + args = self.parser.parse_args(argv) + files = [fp for fp in args.files] + + self.assertEqual(len(files), 1) + self.assertTrue(GITTEST_FP in files) + + @assert_no_side_effects(SYMLINK_TARGET_FP) + def test_path_processor_track_symlink(self): + argv = ['track', SYMLINK_FP] + args = self.parser.parse_args(argv) + files = [fp for fp in args.files] + + if os.path.exists(SYMLINK_FP): + self.assertEqual(len(files), 1) + self.assertTrue(SYMLINK_FP in files) + self.assertFalse(SYMLINK_TARGET_FP in files) + + @assert_no_side_effects(SYMLINK_TARGET_FP) + def test_path_processor_track_symlink_dir(self): + argv = ['track', SYMLINK_DIR] + args = self.parser.parse_args(argv) + files = [fp for fp in args.files] + + if os.path.exists(SYMLINK_FP): + self.assertEqual(len(files), 1) + self.assertTrue(SYMLINK_FP in files) + self.assertFalse(SYMLINK_TARGET_FP in files) + # Unit tests for branch related operations @@ -756,14 +856,14 @@ def setUp(self): # Build up an interesting mock repo. utils_lib.write_file(TRACKED_FP, contents=TRACKED_FP_CONTENTS_1) - git.add(TRACKED_FP) - git.commit(TRACKED_FP, m='1') + utils_lib.git('add', TRACKED_FP) + utils_lib.git('commit', TRACKED_FP, '-m', '1') utils_lib.write_file(TRACKED_FP, contents=TRACKED_FP_CONTENTS_2) - git.commit(TRACKED_FP, m='2') + utils_lib.git('commit', TRACKED_FP, '-m', '2') utils_lib.write_file(UNTRACKED_FP, contents=UNTRACKED_FP_CONTENTS) utils_lib.write_file('.gitignore', contents='{0}'.format(IGNORED_FP)) utils_lib.write_file(IGNORED_FP) - git.branch(BRANCH) + utils_lib.git('branch', BRANCH) self.curr_b = self.repo.current_branch @@ -841,7 +941,7 @@ def test_switch_contents_still_there_ignored(self): def test_switch_contents_still_there_tracked_commit(self): utils_lib.write_file(TRACKED_FP, contents='commit') - git.commit(TRACKED_FP, m='comment') + utils_lib.git('commit', TRACKED_FP, '-m', 'comment') self.repo.switch_current_branch(self.repo.lookup_branch(BRANCH)) self.assertEqual(TRACKED_FP_CONTENTS_2, utils_lib.read_file(TRACKED_FP)) self.repo.switch_current_branch(self.repo.lookup_branch('master')) @@ -917,7 +1017,7 @@ class TestRemoteList(TestRemote): def test_list_all(self): self.remotes.create('remote1', self.remote_path) self.remotes.create('remote2', self.remote_path) - self.assertItemsEqual( + self.assertCountEqual( ['remote1', 'remote2'], [r.name for r in self.remotes]) @@ -940,8 +1040,8 @@ def setUp(self): super(TestRemoteSync, self).setUp() utils_lib.write_file('foo', contents='foo') - git.add('foo') - git.commit('foo', m='msg') + utils_lib.git('add', 'foo') + utils_lib.git('commit', 'foo', '-m', 'msg') self.repo.remotes.create('remote', self.remote_path) self.remote = self.repo.remotes['remote'] @@ -955,11 +1055,11 @@ def test_sync_changes(self): # It is not a ff so it should fail self.assertRaises(core.GlError, current_b.publish, remote_branch) # Get the changes - git.rebase(remote_branch) + utils_lib.git('rebase', str(remote_branch)) # Retry (this time it should work) current_b.publish(remote_branch) - self.assertItemsEqual( + self.assertCountEqual( ['master', REMOTE_BRANCH], self.remote.listall_branches()) self.assertEqual( master_head_before.id, self.remote.lookup_branch('master').head.id) diff --git a/gitless/tests/test_e2e.py b/gitless/tests/test_e2e.py index 9a9333b..a2daeb3 100755 --- a/gitless/tests/test_e2e.py +++ b/gitless/tests/test_e2e.py @@ -5,21 +5,12 @@ """End-to-end test.""" -from __future__ import unicode_literals - import logging import os import re import time - +from subprocess import CalledProcessError import sys -if sys.platform != 'win32': - from sh import ErrorReturnCode, gl, git -else: - from pbs import ErrorReturnCode, Command - gl = Command('gl') - git = Command('git') - from gitless.tests import utils @@ -33,18 +24,18 @@ class TestEndToEnd(utils.TestBase): def setUp(self): super(TestEndToEnd, self).setUp('gl-e2e-test') - gl.init() + utils.gl('init') # Disable colored output so that we don't need to worry about ANSI escape # codes - git.config('color.ui', False) + utils.git('config', 'color.ui', 'False') # Disable paging so that we don't have to use sh's _tty_out option, which is # not available on pbs if sys.platform != 'win32': - git.config('core.pager', 'cat') + utils.git('config', 'core.pager', 'cat') else: # On Windows, we need to call 'type' through cmd.exe (with 'cmd'). The /C # is so that the command window gets closed after 'type' finishes - git.config('core.pager', 'cmd /C type') + utils.git('config', 'core.pager', 'cmd /C type') utils.set_test_config() @@ -57,11 +48,11 @@ def test_not_in_repo(self): def assert_not_in_repo(*cmds): for cmd in cmds: self.assertRaisesRegexp( - ErrorReturnCode, 'not in a Gitless\'s repository', cmd) + CalledProcessError, 'not in a Gitless\'s repository', utils.gl, cmd) assert_not_in_repo( - gl.status, gl.diff, gl.commit, gl.branch, gl.merge, gl.fuse, gl.remote, - gl.publish, gl.history) + 'status', 'diff', 'commit', 'branch', 'merge', 'fuse', 'remote', + 'publish', 'history') class TestBasic(TestEndToEnd): @@ -69,91 +60,104 @@ class TestBasic(TestEndToEnd): def test_basic_functionality(self): utils.write_file('file1', 'Contents of file1') # Track - gl.track('file1') - self.assertRaises(ErrorReturnCode, gl.track, 'file1') - self.assertRaises(ErrorReturnCode, gl.track, 'non-existent') + utils.gl('track', 'file1') + self.assertRaises(CalledProcessError, utils.gl, 'track', 'file1') + self.assertRaises(CalledProcessError, utils.gl, 'track', 'non-existent') # Untrack - gl.untrack('file1') - self.assertRaises(ErrorReturnCode, gl.untrack, 'file1') - self.assertRaises(ErrorReturnCode, gl.untrack, 'non-existent') + utils.gl('untrack', 'file1') + self.assertRaises(CalledProcessError, utils.gl, 'untrack', 'file1') + self.assertRaises(CalledProcessError, utils.gl, 'untrack', 'non-existent') # Commit - gl.track('file1') - gl.commit(m='file1 commit') - self.assertRaises(ErrorReturnCode, gl.commit, m='nothing to commit') + utils.gl('track', 'file1') + utils.gl('commit', '-m', 'file1 commit') + self.assertRaises( + CalledProcessError, utils.gl, 'commit', '-m', 'nothing to commit') # History - if 'file1 commit' not in utils.stdout(gl.history()): + if 'file1 commit' not in utils.gl('history'): self.fail('Commit didn\'t appear in history') # Branch # Make some changes to file1 and branch out utils.write_file('file1', 'New contents of file1') - gl.branch(c='branch1') - gl.switch('branch1') + utils.gl('branch', '-c', 'branch1') + utils.gl('switch', 'branch1') if 'New' in utils.read_file('file1'): self.fail('Branch not independent!') # Switch back to master branch, check that contents are the same as before. - gl.switch('master') + utils.gl('switch', 'master') if 'New' not in utils.read_file('file1'): self.fail('Branch not independent!') - out = utils.stdout(gl.branch()) + out = utils.gl('branch') if '* master' not in out: self.fail('Branch status output wrong: {0}'.format(out)) if 'branch1' not in out: self.fail('Branch status output wrong: {0}'.format(out)) - gl.branch(c='branch2') - gl.branch(c='branch-conflict1') - gl.branch(c='branch-conflict2') - gl.commit(m='New contents commit') + utils.gl('branch', '-c', 'branch2') + utils.gl('branch', '-c', 'branch-conflict1') + utils.gl('branch', '-c', 'branch-conflict2') + utils.gl('commit', '-m', 'New contents commit') # Fuse - gl.switch('branch1') - self.assertRaises(ErrorReturnCode, gl.fuse) # no upstream set + utils.gl('switch', 'branch1') + self.assertRaises(CalledProcessError, utils.gl, 'fuse') # no upstream set try: - gl.fuse('master') - except ErrorReturnCode as e: + utils.gl('fuse', 'master') + except CalledProcessError as e: self.fail(utils.stderr(e)) - out = utils.stdout(gl.history()) + out = utils.gl('history') if 'file1 commit' not in out: self.fail(out) # Merge - gl.switch('branch2') - self.assertRaises(ErrorReturnCode, gl.merge) # no upstream set - gl.merge('master') - out = utils.stdout(gl.history()) + utils.gl('switch', 'branch2') + self.assertRaises(CalledProcessError, utils.gl, 'merge') # no upstream set + utils.gl('merge', 'master') + out = utils.gl('history') if 'file1 commit' not in out: self.fail(out) # Conflicting fuse - gl.switch('branch-conflict1') + utils.gl('switch', 'branch-conflict1') utils.write_file('file1', 'Conflicting changes to file1') - gl.commit(m='changes in branch-conflict1') - err = utils.stderr(gl.fuse('master', _ok_code=[1])) - if 'conflict' not in err: - self.fail(err) - out = utils.stdout(gl.status()) + utils.gl('commit', '-m', 'changes in branch-conflict1') + try: + utils.gl('fuse', 'master') + except CalledProcessError as e: + # expected + err = e.stderr + if 'conflict' not in err: + self.fail(err) + + out = utils.gl('status') if 'file1 (with conflicts)' not in out: self.fail(out) # Try aborting - gl.fuse('--abort') - out = utils.stdout(gl.status()) + utils.gl('fuse', '--abort') + out = utils.gl('status') if 'file1' in out: self.fail(out) # Ok, now let's fix the conflicts - err = utils.stderr(gl.fuse('master', _ok_code=[1])) - if 'conflict' not in err: - self.fail(err) - out = utils.stdout(gl.status()) + try: + utils.gl('fuse', 'master') + except CalledProcessError as e: + # expected + err = e.stderr + if 'conflict' not in err: + self.fail(err) + + out = utils.gl('status') if 'file1 (with conflicts)' not in out: self.fail(out) utils.write_file('file1', 'Fixed conflicts!') - self.assertRaises(ErrorReturnCode, gl.commit, m='resolve not called') - self.assertRaises(ErrorReturnCode, gl.resolve, 'non-existent') - gl.resolve('file1') - gl.commit(m='fixed conflicts') + self.assertRaises( + CalledProcessError, utils.gl, 'commit', '-m', 'resolve not called') + self.assertRaises( + CalledProcessError, utils.gl, 'resolve', 'non-existent') + utils.gl('resolve', 'file1') + utils.gl('commit', '-m', 'fixed conflicts') class TestCommit(TestEndToEnd): @@ -169,64 +173,71 @@ def setUp(self): utils.write_file(self.TRACKED_FP) utils.write_file(self.DIR_TRACKED_FP) utils.write_file(self.UNTRACKED_FP) - gl.track(self.TRACKED_FP, self.DIR_TRACKED_FP) + utils.gl('track', self.TRACKED_FP, self.DIR_TRACKED_FP) def test_commit(self): - gl.commit(m='msg') + utils.gl('commit', '-m', 'msg') self.__assert_commit(self.TRACKED_FP, self.DIR_TRACKED_FP) def test_commit_relative(self): os.chdir(self.DIR) - gl.commit(m='msg') + utils.gl('commit', '-m', 'msg') self.__assert_commit(self.TRACKED_FP, self.DIR_TRACKED_FP) def test_commit_only(self): - gl.commit(self.TRACKED_FP, m="msg") + utils.gl('commit', self.TRACKED_FP, '-m', 'msg') self.__assert_commit(self.TRACKED_FP) def test_commit_only_relative(self): os.chdir(self.DIR) - self.assertRaises(ErrorReturnCode, gl.commit, self.TRACKED_FP, "-m='msg'") - gl.commit('../' + self.TRACKED_FP, m='msg') + self.assertRaises( + CalledProcessError, utils.gl, 'commit', self.TRACKED_FP, '-m', 'msg') + utils.gl('commit', '../' + self.TRACKED_FP, '-m', 'msg') self.__assert_commit(self.TRACKED_FP) def test_commit_only_untrack(self): - gl.commit("-m='msg'", self.UNTRACKED_FP) + utils.gl('commit', '-m', 'msg', self.UNTRACKED_FP) self.__assert_commit(self.UNTRACKED_FP) def test_commit_only_untrack_relative(self): os.chdir(self.DIR) - self.assertRaises(ErrorReturnCode, gl.commit, self.UNTRACKED_FP, m='msg') - gl.commit('../' + self.UNTRACKED_FP, m='msg') + self.assertRaises( + CalledProcessError, utils.gl, 'commit', self.UNTRACKED_FP, '-m', 'msg') + utils.gl('commit', '../' + self.UNTRACKED_FP, '-m', 'msg') self.__assert_commit(self.UNTRACKED_FP) def test_commit_include(self): - gl.commit("-m='msg'", include=self.UNTRACKED_FP) + utils.gl('commit', '-m', 'msg', '--include', self.UNTRACKED_FP) self.__assert_commit( self.TRACKED_FP, self.DIR_TRACKED_FP, self.UNTRACKED_FP) def test_commit_exclude_include(self): - gl.commit("-m='msg'", include=self.UNTRACKED_FP, exclude=self.TRACKED_FP) + utils.gl( + 'commit', '-m', 'msg', + '--include', self.UNTRACKED_FP, '--exclude', self.TRACKED_FP) self.__assert_commit(self.UNTRACKED_FP, self.DIR_TRACKED_FP) def test_commit_no_files(self): self.assertRaises( - ErrorReturnCode, gl.commit, '--exclude', - self.TRACKED_FP, self.DIR_TRACKED_FP, m='msg') - self.assertRaises(ErrorReturnCode, gl.commit, 'non-existent', m='msg') + CalledProcessError, utils.gl, 'commit', '--exclude', + self.TRACKED_FP, self.DIR_TRACKED_FP, '-m', 'msg') + self.assertRaises( + CalledProcessError, utils.gl, 'commit', 'non-existent', '-m', 'msg') self.assertRaises( - ErrorReturnCode, gl.commit, m='msg', exclude='non-existent') + CalledProcessError, utils.gl, 'commit', '-m', 'msg', + '--exclude', 'non-existent') self.assertRaises( - ErrorReturnCode, gl.commit, m='msg', include='non-existent') + CalledProcessError, utils.gl, 'commit', '-m', 'msg', + '--include', 'non-existent') def test_commit_dir(self): fp = 'dir/f' utils.write_file(fp) - gl.commit(fp, m='msg') + utils.gl('commit', fp, '-m', 'msg') self.__assert_commit('dir/f') def __assert_commit(self, *expected_committed): - h = utils.stdout(gl.history(v=True)) + h = utils.gl('history', '-v') for fp in expected_committed: if fp not in h: self.fail('{0} was apparently not committed!'.format(fp)) @@ -247,11 +258,11 @@ def setUp(self): super(TestStatus, self).setUp() utils.write_file(self.TRACKED_DIR_FP) utils.write_file(self.UNTRACKED_DIR_FP) - gl.commit(self.TRACKED_DIR_FP, m='commit') + utils.gl('commit', self.TRACKED_DIR_FP, '-m', 'commit') def test_status_relative(self): utils.write_file(self.TRACKED_DIR_FP, contents='some modifications') - st = utils.stdout(gl.status()) + st = utils.gl('status') if self.TRACKED_DIR_FP not in st: self.fail() if self.UNTRACKED_DIR_FP not in st: @@ -259,7 +270,7 @@ def test_status_relative(self): os.chdir(self.DIR) - st = utils.stdout(gl.status()) + st = utils.gl('status') rel_tracked = os.path.relpath(self.TRACKED_DIR_FP, self.DIR) rel_untracked = os.path.relpath(self.UNTRACKED_DIR_FP, self.DIR) if (self.TRACKED_DIR_FP in st) or (rel_tracked not in st): @@ -276,36 +287,40 @@ class TestBranch(TestEndToEnd): def setUp(self): super(TestBranch, self).setUp() utils.write_file('f') - gl.commit('f', m='commit') + utils.gl('commit', 'f', '-m', 'commit') def test_create(self): - gl.branch(c=self.BRANCH_1) - self.assertRaises(ErrorReturnCode, gl.branch, c=self.BRANCH_1) - self.assertRaises(ErrorReturnCode, gl.branch, c='evil*named*branch') - if self.BRANCH_1 not in utils.stdout(gl.branch()): + utils.gl('branch', '-c', self.BRANCH_1) + self.assertRaises( + CalledProcessError, utils.gl, 'branch', '-c', self.BRANCH_1) + self.assertRaises( + CalledProcessError, utils.gl, 'branch', '-c', 'evil*named*branch') + if self.BRANCH_1 not in utils.gl('branch'): self.fail() def test_remove(self): - gl.branch(c=self.BRANCH_1) - gl.switch(self.BRANCH_1) - self.assertRaises(ErrorReturnCode, gl.branch, d=self.BRANCH_1, _in='y') - gl.branch(c=self.BRANCH_2) - gl.switch(self.BRANCH_2) - gl.branch(d=self.BRANCH_1, _in='n') - gl.branch(d=self.BRANCH_1, _in='y') - if self.BRANCH_1 in utils.stdout(gl.branch()): + utils.gl('branch', '-c', self.BRANCH_1) + utils.gl('switch', self.BRANCH_1) + self.assertRaises( + CalledProcessError, utils.gl, 'branch', '-d', self.BRANCH_1, _in='y') + utils.gl('branch', '-c', self.BRANCH_2) + utils.gl('switch', self.BRANCH_2) + utils.gl('branch', '-d', self.BRANCH_1, _in='n') + utils.gl('branch', '-d', self.BRANCH_1, _in='y') + if self.BRANCH_1 in utils.gl('branch'): self.fail() def test_upstream(self): - self.assertRaises(ErrorReturnCode, gl.branch, '-uu') - self.assertRaises(ErrorReturnCode, gl.branch, '-su', 'non-existent') + self.assertRaises(CalledProcessError, utils.gl, 'branch', '-uu') + self.assertRaises( + CalledProcessError, utils.gl, 'branch', '-su', 'non-existent') self.assertRaises( - ErrorReturnCode, gl.branch, '-su', 'non-existent/non-existent') + CalledProcessError, utils.gl, 'branch', '-su', 'non-existent/non-existent') def test_list(self): - gl.branch(c=self.BRANCH_1) - gl.branch(c=self.BRANCH_2) - branch_out = utils.stdout(gl.branch()) + utils.gl('branch', '-c', self.BRANCH_1) + utils.gl('branch', '-c', self.BRANCH_2) + branch_out = utils.gl('branch') self.assertTrue( branch_out.find(self.BRANCH_1) < branch_out.find(self.BRANCH_2)) @@ -318,26 +333,27 @@ class TestTag(TestEndToEnd): def setUp(self): super(TestTag, self).setUp() utils.write_file('f') - gl.commit('f', m='commit') + utils.gl('commit', 'f', '-m', 'commit') def test_create(self): - gl.tag(c=self.TAG_1) - self.assertRaises(ErrorReturnCode, gl.tag, c=self.TAG_1) - self.assertRaises(ErrorReturnCode, gl.tag, c='evil*named*tag') - if self.TAG_1 not in utils.stdout(gl.tag()): + utils.gl('tag', '-c', self.TAG_1) + self.assertRaises(CalledProcessError, utils.gl, 'tag', '-c', self.TAG_1) + self.assertRaises( + CalledProcessError, utils.gl, 'tag', '-c', 'evil*named*tag') + if self.TAG_1 not in utils.gl('tag'): self.fail() def test_remove(self): - gl.tag(c=self.TAG_1) - gl.tag(d=self.TAG_1, _in='n') - gl.tag(d=self.TAG_1, _in='y') - if self.TAG_1 in utils.stdout(gl.tag()): + utils.gl('tag', '-c', self.TAG_1) + utils.gl('tag', '-d', self.TAG_1, _in='n') + utils.gl('tag', '-d', self.TAG_1, _in='y') + if self.TAG_1 in utils.gl('tag'): self.fail() def test_list(self): - gl.tag(c=self.TAG_1) - gl.tag(c=self.TAG_2) - tag_out = utils.stdout(gl.tag()) + utils.gl('tag', '-c', self.TAG_1) + utils.gl('tag', '-c', self.TAG_2) + tag_out = utils.gl('tag') self.assertTrue( tag_out.find(self.TAG_1) < tag_out.find(self.TAG_2)) @@ -353,24 +369,28 @@ def setUp(self): super(TestDiffFile, self).setUp() utils.write_file(self.TRACKED_FP) utils.write_file(self.DIR_TRACKED_FP) - gl.commit(self.TRACKED_FP, self.DIR_TRACKED_FP, m='commit') + utils.gl('commit', self.TRACKED_FP, self.DIR_TRACKED_FP, '-m', 'commit') utils.write_file(self.UNTRACKED_FP) def test_empty_diff(self): - if 'No files to diff' not in utils.stdout(gl.diff()): + if 'No files to diff' not in utils.gl('diff'): self.fail() def test_diff_nonexistent_fp(self): - err = utils.stderr(gl.diff('file', _ok_code=[1])) - if 'doesn\'t exist' not in err: - self.fail() + try: + utils.gl('diff', 'file') + except CalledProcessError as e: + # expected + err = e.stderr + if 'doesn\'t exist' not in err: + self.fail() def test_basic_diff(self): utils.write_file(self.TRACKED_FP, contents='contents') - out1 = utils.stdout(gl.diff()) + out1 = utils.gl('diff') if '+contents' not in out1: self.fail() - out2 = utils.stdout(gl.diff(self.TRACKED_FP)) + out2 = utils.gl('diff', self.TRACKED_FP) if '+contents' not in out2: self.fail() self.assertEqual(out1, out2) @@ -379,20 +399,20 @@ def test_basic_diff_relative(self): utils.write_file(self.TRACKED_FP, contents='contents_tracked') utils.write_file(self.DIR_TRACKED_FP, contents='contents_dir_tracked') os.chdir(self.DIR) - out1 = utils.stdout(gl.diff()) + out1 = utils.gl('diff') if '+contents_tracked' not in out1: self.fail() if '+contents_dir_tracked' not in out1: self.fail() rel_dir_tracked_fp = os.path.relpath(self.DIR_TRACKED_FP, self.DIR) - out2 = utils.stdout(gl.diff(rel_dir_tracked_fp)) + out2 = utils.gl('diff', rel_dir_tracked_fp) if '+contents_dir_tracked' not in out2: self.fail() def test_diff_dir(self): fp = 'dir/dir/f' utils.write_file(fp, contents='contents') - out = utils.stdout(gl.diff(fp)) + out = utils.gl('diff', fp) if '+contents' not in out: self.fail() @@ -402,10 +422,10 @@ def test_diff_non_ascii(self): return contents = '’◕‿◕’©Ä☺’ಠ_ಠ’' utils.write_file(self.TRACKED_FP, contents=contents) - out1 = utils.stdout(gl.diff()) + out1 = utils.gl('diff') if '+' + contents not in out1: self.fail('out is ' + out1) - out2 = utils.stdout(gl.diff(self.TRACKED_FP)) + out2 = utils.gl('diff', self.TRACKED_FP) if '+' + contents not in out2: self.fail('out is ' + out2) self.assertEqual(out1, out2) @@ -425,31 +445,35 @@ def setUp(self): def create_commits(branch_name, fp): self.commits[branch_name] = [] utils.append_to_file(fp, contents='contents {0}\n'.format(0)) - out = utils.stdout(gl.commit(m='ci 0 in {0}'.format(branch_name), inc=fp)) + out = utils.gl( + 'commit', '-m', 'ci 0 in {0}'.format(branch_name), '--include', fp) self.commits[branch_name].append( re.search(r'Commit Id: (\S*)', out, re.UNICODE).group(1)) for i in range(1, self.COMMITS_NUMBER): utils.append_to_file(fp, contents='contents {0}\n'.format(i)) - out = utils.stdout(gl.commit(m='ci {0} in {1}'.format(i, branch_name))) + out = utils.gl('commit', '-m', 'ci {0} in {1}'.format(i, branch_name)) self.commits[branch_name].append( re.search(r'Commit Id: (\S*)', out, re.UNICODE).group(1)) - gl.branch(c=self.OTHER) + utils.gl('branch', '-c', self.OTHER) create_commits('master', self.MASTER_FILE) - gl.switch(self.OTHER) + try: + utils.gl('switch', self.OTHER) + except CalledProcessError as e: + raise Exception(e.stderr) create_commits(self.OTHER, self.OTHER_FILE) - gl.switch('master') + utils.gl('switch', 'master') class TestFuse(TestOp): def __assert_history(self, expected): - out = utils.stdout(gl.history()) + out = utils.gl('history') cids = list(reversed(re.findall(r'ci (.*) in (\S*)', out, re.UNICODE))) - self.assertItemsEqual( + self.assertCountEqual( cids, expected, 'cids is ' + text(cids) + ' exp ' + text(expected)) - st_out = utils.stdout(gl.status()) + st_out = utils.gl('status') self.assertFalse('fuse' in st_out) def __build(self, branch_name, cids=None): @@ -458,50 +482,54 @@ def __build(self, branch_name, cids=None): return [(text(ci), branch_name) for ci in cids] def test_basic(self): - gl.fuse(self.OTHER) + utils.gl('fuse', self.OTHER) self.__assert_history(self.__build(self.OTHER) + self.__build('master')) def test_only_errors(self): - self.assertRaises(ErrorReturnCode, gl.fuse, self.OTHER, o='non-existent-id') self.assertRaises( - ErrorReturnCode, gl.fuse, self.OTHER, o=self.commits['master'][1]) + CalledProcessError, utils.gl, 'fuse', self.OTHER, '-o', 'non-existent-id') + self.assertRaises( + CalledProcessError, utils.gl, 'fuse', self.OTHER, + '-o', self.commits['master'][1]) def test_only_one(self): - gl.fuse(self.OTHER, o=self.commits[self.OTHER][0]) + utils.gl('fuse', self.OTHER, '-o', self.commits[self.OTHER][0]) self.__assert_history( self.__build(self.OTHER, cids=[0]) + self.__build('master')) def test_only_some(self): - gl.fuse(self.OTHER, '-o', self.commits[self.OTHER][:2]) + utils.gl('fuse', self.OTHER, '-o', *self.commits[self.OTHER][:2]) self.__assert_history( self.__build(self.OTHER, [0, 1]) + self.__build('master')) def test_exclude_errors(self): - self.assertRaises(ErrorReturnCode, gl.fuse, self.OTHER, e='non-existent-id') self.assertRaises( - ErrorReturnCode, gl.fuse, self.OTHER, e=self.commits['master'][1]) + CalledProcessError, utils.gl, 'fuse', self.OTHER, '-e', 'non-existent-id') + self.assertRaises( + CalledProcessError, utils.gl, 'fuse', self.OTHER, + '-e', self.commits['master'][1]) def test_exclude_one(self): last_ci = self.COMMITS_NUMBER - 1 - gl.fuse(self.OTHER, e=self.commits[self.OTHER][last_ci]) + utils.gl('fuse', self.OTHER, '-e', self.commits[self.OTHER][last_ci]) self.__assert_history( self.__build(self.OTHER, range(0, last_ci)) + self.__build('master')) def test_exclude_some(self): - gl.fuse(self.OTHER, '-e', self.commits[self.OTHER][1:]) + utils.gl('fuse', self.OTHER, '-e', *self.commits[self.OTHER][1:]) self.__assert_history( self.__build(self.OTHER, cids=[0]) + self.__build('master')) def test_ip_dp(self): - gl.fuse(self.OTHER, insertion_point='dp') + utils.gl('fuse', self.OTHER, '--insertion-point', 'dp') self.__assert_history(self.__build(self.OTHER) + self.__build('master')) def test_ip_head(self): - gl.fuse(self.OTHER, insertion_point='HEAD') + utils.gl('fuse', self.OTHER, '--insertion-point', 'HEAD') self.__assert_history(self.__build('master') + self.__build(self.OTHER)) def test_ip_commit(self): - gl.fuse(self.OTHER, insertion_point=self.commits['master'][1]) + utils.gl('fuse', self.OTHER, '--insertion-point', self.commits['master'][1]) self.__assert_history( self.__build('master', [0, 1]) + self.__build(self.OTHER) + self.__build('master', range(2, self.COMMITS_NUMBER))) @@ -509,88 +537,91 @@ def test_ip_commit(self): def test_conflicts(self): def trigger_conflicts(): self.assertRaisesRegexp( - ErrorReturnCode, 'conflicts', gl.fuse, - self.OTHER, e=self.commits[self.OTHER][0]) + CalledProcessError, 'conflicts', utils.gl, 'fuse', + self.OTHER, '-e', self.commits[self.OTHER][0]) # Abort trigger_conflicts() - gl.fuse('-a') + utils.gl('fuse', '-a') self.__assert_history(self.__build('master')) # Fix conflicts trigger_conflicts() - gl.resolve(self.OTHER_FILE) - gl.commit(m='ci 1 in other') + utils.gl('resolve', self.OTHER_FILE) + utils.gl('commit', '-m', 'ci 1 in other') self.__assert_history( self.__build(self.OTHER, range(1, self.COMMITS_NUMBER)) + self.__build('master')) def test_conflicts_switch(self): - gl.switch('other') + utils.gl('switch', 'other') utils.write_file(self.OTHER_FILE, contents='uncommitted') - gl.switch('master') + utils.gl('switch', 'master') try: - gl.fuse(self.OTHER, e=self.commits[self.OTHER][0]) + utils.gl('fuse', self.OTHER, '-e', self.commits[self.OTHER][0]) self.fail() - except ErrorReturnCode: + except CalledProcessError: pass # Switch - gl.switch('other') + utils.gl('switch', 'other') self.__assert_history(self.__build('other')) - st_out = utils.stdout(gl.status()) + st_out = utils.gl('status') self.assertTrue('fuse' not in st_out) self.assertTrue('conflict' not in st_out) - gl.switch('master') - st_out = utils.stdout(gl.status()) + utils.gl('switch', 'master') + st_out = utils.gl('status') self.assertTrue('fuse' in st_out) self.assertTrue('conflict' in st_out) # Check that we are able to complete the fuse after switch - gl.resolve(self.OTHER_FILE) - gl.commit(m='ci 1 in other') + utils.gl('resolve', self.OTHER_FILE) + utils.gl('commit', '-m', 'ci 1 in other') self.__assert_history( self.__build(self.OTHER, range(1, self.COMMITS_NUMBER)) + self.__build('master')) - gl.switch('other') + utils.gl('switch', 'other') self.assertEqual('uncommitted', utils.read_file(self.OTHER_FILE)) def test_conflicts_multiple(self): - gl.branch(c='tmp', divergent_point='HEAD~2') - gl.switch('tmp') + utils.gl('branch', '-c', 'tmp', '--divergent-point', 'HEAD~2') + utils.gl('switch', 'tmp') utils.append_to_file(self.MASTER_FILE, contents='conflict') - gl.commit(m='will conflict 0') + utils.gl('commit', '-m', 'will conflict 0') utils.append_to_file(self.MASTER_FILE, contents='conflict') - gl.commit(m='will conflict 1') + utils.gl('commit', '-m', 'will conflict 1') - self.assertRaisesRegexp(ErrorReturnCode, 'conflicts', gl.fuse, 'master') - gl.resolve(self.MASTER_FILE) self.assertRaisesRegexp( - ErrorReturnCode, 'conflicts', gl.commit, m='ci 0 in tmp') - gl.resolve(self.MASTER_FILE) - gl.commit(m='ci 1 in tmp') # this one should finalize the fuse + CalledProcessError, 'conflicts', utils.gl, 'fuse', 'master') + utils.gl('resolve', self.MASTER_FILE) + self.assertRaisesRegexp( + CalledProcessError, 'conflicts', utils.gl, 'commit', '-m', 'ci 0 in tmp') + utils.gl('resolve', self.MASTER_FILE) + utils.gl('commit', '-m', 'ci 1 in tmp') # this one should finalize the fuse self.__assert_history( self.__build('master') + self.__build('tmp', range(2))) def test_conflicts_multiple_uncommitted_changes(self): - gl.branch(c='tmp', divergent_point='HEAD~2') - gl.switch('tmp') + utils.gl('branch', '-c', 'tmp', '--divergent-point', 'HEAD~2') + utils.gl('switch', 'tmp') utils.append_to_file(self.MASTER_FILE, contents='conflict') - gl.commit(m='will conflict 0') + utils.gl('commit', '-m', 'will conflict 0') utils.append_to_file(self.MASTER_FILE, contents='conflict') - gl.commit(m='will conflict 1') + utils.gl('commit', '-m', 'will conflict 1') utils.write_file(self.MASTER_FILE, contents='uncommitted') - self.assertRaisesRegexp(ErrorReturnCode, 'conflicts', gl.fuse, 'master') - gl.resolve(self.MASTER_FILE) self.assertRaisesRegexp( - ErrorReturnCode, 'conflicts', gl.commit, m='ci 0 in tmp') - gl.resolve(self.MASTER_FILE) + CalledProcessError, 'conflicts', utils.gl, 'fuse', 'master') + utils.gl('resolve', self.MASTER_FILE) + self.assertRaisesRegexp( + CalledProcessError, 'conflicts', utils.gl, 'commit', '-m', 'ci 0 in tmp') + utils.gl('resolve', self.MASTER_FILE) self.assertRaisesRegexp( - ErrorReturnCode, 'failed to apply', gl.commit, m='ci 1 in tmp') + CalledProcessError, 'failed to apply', utils.gl, + 'commit', '-m', 'ci 1 in tmp') self.__assert_history( self.__build('master') + self.__build('tmp', range(2))) @@ -598,48 +629,48 @@ def test_conflicts_multiple_uncommitted_changes(self): def test_nothing_to_fuse(self): self.assertRaisesRegexp( - ErrorReturnCode, 'No commits to fuse', gl.fuse, - self.OTHER, '-e', *self.commits[self.OTHER]) + CalledProcessError, 'No commits to fuse', utils.gl, 'fuse', + self.OTHER, '-e', *self.commits[self.OTHER]) def test_ff(self): - gl.branch(c='tmp', divergent_point='HEAD~2') - gl.switch('tmp') + utils.gl('branch', '-c', 'tmp', '--divergent-point', 'HEAD~2') + utils.gl('switch', 'tmp') - gl.fuse('master') + utils.gl('fuse', 'master') self.__assert_history(self.__build('master')) def test_ff_ip_head(self): - gl.branch(c='tmp', divergent_point='HEAD~2') - gl.switch('tmp') + utils.gl('branch', '-c', 'tmp', '--divergent-point', 'HEAD~2') + utils.gl('switch', 'tmp') - gl.fuse('master', insertion_point='HEAD') + utils.gl('fuse', 'master', '--insertion-point', 'HEAD') self.__assert_history(self.__build('master')) def test_uncommitted_changes(self): utils.write_file(self.MASTER_FILE, contents='uncommitted') utils.write_file('master_untracked', contents='uncommitted') - gl.fuse(self.OTHER) + utils.gl('fuse', self.OTHER) self.assertEqual('uncommitted', utils.read_file(self.MASTER_FILE)) self.assertEqual('uncommitted', utils.read_file('master_untracked')) def test_uncommitted_tracked_changes_that_conflict(self): - gl.branch(c='tmp', divergent_point='HEAD~1') - gl.switch('tmp') + utils.gl('branch', '-c', 'tmp', '--divergent-point', 'HEAD~1') + utils.gl('switch', 'tmp') utils.write_file(self.MASTER_FILE, contents='uncommitted') self.assertRaisesRegexp( - ErrorReturnCode, 'failed to apply', gl.fuse, - 'master', insertion_point='HEAD') + CalledProcessError, 'failed to apply', utils.gl, 'fuse', + 'master', '--insertion-point', 'HEAD') contents = utils.read_file(self.MASTER_FILE) self.assertTrue('uncommitted' in contents) self.assertTrue('contents 2' in contents) def test_uncommitted_tracked_changes_that_conflict_append(self): - gl.branch(c='tmp', divergent_point='HEAD~1') - gl.switch('tmp') + utils.gl('branch', '-c', 'tmp', '--divergent-point', 'HEAD~1') + utils.gl('switch', 'tmp') utils.append_to_file(self.MASTER_FILE, contents='uncommitted') self.assertRaisesRegexp( - ErrorReturnCode, 'failed to apply', gl.fuse, - 'master', insertion_point='HEAD') + CalledProcessError, 'failed to apply', utils.gl, 'fuse', + 'master', '--insertion-point', 'HEAD') contents = utils.read_file(self.MASTER_FILE) self.assertTrue('uncommitted' in contents) self.assertTrue('contents 2' in contents) @@ -647,9 +678,9 @@ def test_uncommitted_tracked_changes_that_conflict_append(self): # def test_uncommitted_untracked_changes_that_conflict(self): # utils.write_file(self.OTHER_FILE, contents='uncommitted in master') # try: -# gl.fuse(self.OTHER) +# utils.gl('fuse', self.OTHER) # self.fail() -# except ErrorReturnCode as e: +# except CalledProcessError as e: # self.assertTrue('failed to apply' in utils.stderr(e)) @@ -658,26 +689,26 @@ class TestMerge(TestOp): def test_uncommitted_changes(self): utils.write_file(self.MASTER_FILE, contents='uncommitted') utils.write_file('master_untracked', contents='uncommitted') - gl.merge(self.OTHER) + utils.gl('merge', self.OTHER) self.assertEqual('uncommitted', utils.read_file(self.MASTER_FILE)) self.assertEqual('uncommitted', utils.read_file('master_untracked')) def test_uncommitted_tracked_changes_that_conflict(self): - gl.branch(c='tmp', divergent_point='HEAD~1') - gl.switch('tmp') + utils.gl('branch', '-c', 'tmp', '--divergent-point', 'HEAD~1') + utils.gl('switch', 'tmp') utils.write_file(self.MASTER_FILE, contents='uncommitted') self.assertRaisesRegexp( - ErrorReturnCode, 'failed to apply', gl.merge, 'master') + CalledProcessError, 'failed to apply', utils.gl, 'merge', 'master') contents = utils.read_file(self.MASTER_FILE) self.assertTrue('uncommitted' in contents) self.assertTrue('contents 2' in contents) def test_uncommitted_tracked_changes_that_conflict_append(self): - gl.branch(c='tmp', divergent_point='HEAD~1') - gl.switch('tmp') + utils.gl('branch', '-c', 'tmp', '--divergent-point', 'HEAD~1') + utils.gl('switch', 'tmp') utils.append_to_file(self.MASTER_FILE, contents='uncommitted') self.assertRaisesRegexp( - ErrorReturnCode, 'failed to apply', gl.merge, 'master') + CalledProcessError, 'failed to apply', utils.gl, 'merge', 'master') contents = utils.read_file(self.MASTER_FILE) self.assertTrue('uncommitted' in contents) self.assertTrue('contents 2' in contents) @@ -700,11 +731,11 @@ def assert_status_performance(): MAX_TOLERANCE = 100 t = time.time() - gl.status() + utils.gl('status') gl_t = time.time() - t t = time.time() - git.status() + utils.git('status') git_t = time.time() - t self.assertTrue( @@ -715,28 +746,28 @@ def assert_status_performance(): assert_status_performance() # Track all files, repeat logging.info('Doing a massive git add, this might take a while') - git.add('.') + utils.git('add', '.') logging.info('Done') assert_status_performance() def test_branch_switch_performance(self): MAX_TOLERANCE = 100 - gl.commit('f1', m='commit') + utils.gl('commit', 'f1', '-m', 'commit') t = time.time() - gl.branch(c='develop') - gl.switch('develop') + utils.gl('branch', '-c', 'develop') + utils.gl('switch', 'develop') gl_t = time.time() - t # go back to previous state - gl.switch('master') + utils.gl('switch', 'master') # do the same for git t = time.time() - git.branch('gitdev') - git.stash.save('--all') - git.checkout('gitdev') + utils.git('branch', 'gitdev') + utils.git('stash', 'save', '--all') + utils.git('checkout', 'gitdev') git_t = time.time() - t self.assertTrue( diff --git a/gitless/tests/utils.py b/gitless/tests/utils.py index 30602d0..a1d9d28 100644 --- a/gitless/tests/utils.py +++ b/gitless/tests/utils.py @@ -5,8 +5,6 @@ """Utility library for tests.""" -from __future__ import unicode_literals - import io from locale import getpreferredencoding import logging @@ -18,14 +16,9 @@ import tempfile import unittest -if sys.platform != 'win32': - from sh import git, ErrorReturnCode -else: - from pbs import ErrorReturnCode, Command - git = Command('git') +from subprocess import run, CalledProcessError -IS_PY2 = sys.version_info[0] == 2 ENCODING = getpreferredencoding() or 'utf-8' @@ -42,24 +35,12 @@ def tearDown(self): """Removes the temporary dir.""" rmtree(self.path) - # Python 2/3 compatibility - def assertItemsEqual(self, actual, expected, msg=None): - try: - return super(TestBase, self).assertItemsEqual(actual, expected, msg=msg) - except AttributeError: - try: - # Checks that actual and expected have the same elements in the same - # number, regardless of their order - return super(TestBase, self).assertCountEqual(actual, expected, msg=msg) - except AttributeError: - return self.assertEqual(sorted(actual), sorted(expected), msg=msg) - def assertRaisesRegexp(self, exc, r, fun, *args, **kwargs): try: fun(*args, **kwargs) self.fail('Exception not raised') except exc as e: - msg = stderr(e) if isinstance(e, ErrorReturnCode) else str(e) + msg = e.stderr if isinstance(e, CalledProcessError) else str(e) if not re.search(r, msg): self.fail('No "{0}" found in "{1}"'.format(r, msg)) @@ -82,6 +63,22 @@ def onerror(func, path, unused_exc_info): # error handler for rmtree logging.debug('Removed dir {0}'.format(path)) +def symlink(src, dst): + try: + os.symlink(src, dst) + except (AttributeError, NotImplementedError, OSError): + # Swallow the exceptions, because Windows is very weird about creating + # symlinks. Python 2 does not have a symlink method on in the os module, + # AttributeError will handle that. Python 3 does have a symlink method in + # the os module, however, it has some quirks. NotImplementedError handles + # the case where the Windows version is prior to Vista. OSError handles the + # case where python doesn't have permissions to create a symlink on + # windows. In all cases, it's not necessary to test this, so skip it. + # See: https://docs.python.org/3.5/library/os.html#os.symlink and + # https://docs.python.org/2.7/library/os.html#os.symlink for full details. + pass + + def write_file(fp, contents=''): _x_file('w', fp, contents=contents) @@ -91,8 +88,8 @@ def append_to_file(fp, contents=''): def set_test_config(): - git.config('user.name', 'test') - git.config('user.email', 'test@test.com') + git('config', 'user.name', 'test') + git('config', 'user.email', 'test@test.com') def read_file(fp): @@ -101,20 +98,24 @@ def read_file(fp): return ret -def stdout(p): - return p.stdout.decode(ENCODING) +def git(*args, cwd=None, _in=None): + p = run( + ['git', '--no-pager', *args], capture_output=True, check=True, cwd=cwd, + input=_in, encoding=ENCODING) + return p.stdout -def stderr(p): - return p.stderr.decode(ENCODING) +def gl(*args, cwd=None, _in=None): + p = run( + ['gl', *args], capture_output=True, check=True, cwd=cwd, + input=_in, encoding=ENCODING) + return p.stdout # Private functions def _x_file(x, fp, contents=''): - assert not IS_PY2 or isinstance(contents, unicode) - if not contents: contents = fp dirs, _ = os.path.split(fp) diff --git a/gl.spec b/gl.spec index dfdee1f..b19c465 100644 --- a/gl.spec +++ b/gl.spec @@ -3,6 +3,11 @@ import os a = Analysis(['gl.py'], pathex=[os.getcwd()], + hiddenimports=[ + # https://github.com/pyinstaller/pyinstaller/issues/3198 + # remove this when dropping support for Python < 3.7 + '_sysconfigdata', + '_cffi_backend'], hookspath=None, runtime_hooks=None) diff --git a/requirements.txt b/requirements.txt index 5de1a5a..2c21c81 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,4 +1,4 @@ -pygit2==0.26.4 # requires libgit2 0.26 -clint==0.5.1 -sh==1.12.14;sys_platform!='win32' -pbs==0.110;sys_platform=='win32' +# make sure to update setup.py if you make any changes to this file + +argcomplete>=1.11.1 +pygit2==1.4.0 # requires libgit2 1.1.x diff --git a/setup.py b/setup.py index 2a16dce..4c4081b 100755 --- a/setup.py +++ b/setup.py @@ -19,7 +19,7 @@ # Build helper if sys.argv[-1] == 'gl-build': - from sh import pyinstaller + from subprocess import run import shutil import tarfile import platform @@ -28,14 +28,15 @@ version, platform.system().lower(), platform.machine()) print('running pyinstaller...') - pyinstaller( - 'gl.spec', clean=True, distpath=rel, _out=sys.stdout, _err=sys.stderr) + run( + ['pyinstaller', 'gl.spec', '--clean', '--distpath', rel], + stdout=sys.stdout, stderr=sys.stderr) print('success!! gl binary should be at {0}/gl'.format(rel)) print('creating tar.gz file') shutil.copy('README.md', rel) shutil.copy('LICENSE.md', rel) - + with tarfile.open(rel + '.tar.gz', 'w:gz') as tar: tar.add(rel) print('success!! binary release at {0}'.format(rel + '.tar.gz')) @@ -44,14 +45,11 @@ ld = """ -Gitless is an experimental version control system built on top of Git. -Many people complain that Git is hard to use. We think the problem lies -deeper than the user interface, in the concepts underlying Git. Gitless -is an experiment to see what happens if you put a simple veneer on an -app that changes the underlying concepts. Because Gitless is implemented -on top of Git (could be considered what Git pros call a \"porcelain\" of -Git), you can always fall back on Git. And of course your coworkers you -share a repo with need never know that you're not a Git aficionado. +Gitless is a version control system built on top of Git, that is easy to learn +and use. It features a simple commit workflow, independent branches, and +a friendly command-line interface. Because Gitless is implemented on top of +Git, you can always fall back on Git. And your coworkers you share a repo with +need never know that you're not a Git aficionado. More info, downloads and documentation @ `Gitless's website `__. @@ -60,27 +58,25 @@ setup( name='gitless', version=version, - description='A version control system built on top of Git', + description='A simple version control system built on top of Git', long_description=ld, author='Santiago Perez De Rosso', author_email='sperezde@csail.mit.edu', - url='http://gitless.com', + url='https://gitless.com', packages=['gitless', 'gitless.cli'], install_requires=[ - 'pygit2>=0.24.0', - 'clint>=0.3.6', - 'sh>=1.11' if sys.platform != 'win32' else 'pbs>=0.11' + # make sure install_requires is consistent with requirements.txt + 'pygit2==1.4.0', # requires libgit2 1.1.x + 'argcomplete>=1.11.1' ], license='MIT', - classifiers=( + classifiers=[ 'Development Status :: 2 - Pre-Alpha', 'Intended Audience :: Developers', 'License :: OSI Approved :: MIT License', 'Natural Language :: English', 'Programming Language :: Python', - 'Programming Language :: Python :: 2', - 'Programming Language :: Python :: 3', - 'Topic :: Software Development :: Version Control'), + 'Topic :: Software Development :: Version Control'], entry_points={ 'console_scripts': [ 'gl = gitless.cli.gl:main' diff --git a/snap/snapcraft.yaml b/snap/snapcraft.yaml index a7e32e8..dba4c8d 100644 --- a/snap/snapcraft.yaml +++ b/snap/snapcraft.yaml @@ -4,14 +4,15 @@ name: gitless version: git summary: A simple version control system built on top of Git description: | - Gitless is an experimental version control system built on top of Git. - Many people complain that Git is hard to use. We think the problem lies - deeper than the user interface, in the concepts underlying Git. Gitless - is an experiment to see what happens if you put a simple veneer on an - app that changes the underlying concepts. Because Gitless is implemented - on top of Git (could be considered what Git pros call a "porcelain" of - Git), you can always fall back on Git. And of course your coworkers you - share a repo with need never know that you're not a Git aficionado. + Gitless is a version control system built on top of Git, that is easy to learn + and use. It features a simple commit workflow, independent branches, and + a friendly command-line interface. Because Gitless is implemented on top of + Git, you can always fall back on Git. And your coworkers you share a repo with + need never know that you're not a Git aficionado. + + +# Base snap for snapd that is based on Ubuntu 18.04 +base: core18 grade: devel # 'stable' for stable/candidate upload confinement: devmode # 'strict' after right plugs and slots @@ -25,7 +26,8 @@ apps: parts: libgit2: plugin: cmake - source: https://github.com/libgit2/libgit2/archive/v0.27.0.tar.gz + # https://www.pygit2.org/install.html#version-numbers + source: https://github.com/libgit2/libgit2/archive/v1.1.0.tar.gz build-packages: - libssl-dev @@ -38,4 +40,3 @@ parts: - git build-packages: - git - diff --git a/tox.ini b/tox.ini deleted file mode 100644 index d3ee346..0000000 --- a/tox.ini +++ /dev/null @@ -1,7 +0,0 @@ -[tox] -envlist = py27, py34 -[testenv] -deps=nose -commands= - nosetests - nosetests gitless/tests/test_e2e.py