From c35cd482284d05d8993636d33ad36768b9f95ab0 Mon Sep 17 00:00:00 2001 From: Jace Browning Date: Thu, 28 Jul 2016 11:35:57 -0400 Subject: [PATCH 01/38] Fix help text on '--clean' --- gitman/cli.py | 2 +- gitman/plugin.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/gitman/cli.py b/gitman/cli.py index e89a3e40..c7c55068 100644 --- a/gitman/cli.py +++ b/gitman/cli.py @@ -35,7 +35,7 @@ def main(args=None, function=None): options.add_argument('-f', '--force', action='store_true', help="overwrite uncommitted changes in dependencies") options.add_argument('-c', '--clean', action='store_true', - help="keep ignored files in dependencies") + help="delete ignored files in dependencies") shared = {'formatter_class': common.WideHelpFormatter} # Main parser diff --git a/gitman/plugin.py b/gitman/plugin.py index 85b87645..27fe3b64 100644 --- a/gitman/plugin.py +++ b/gitman/plugin.py @@ -26,7 +26,7 @@ def main(args=None): ) parser.add_argument( '-c', '--clean', action='store_true', - help="keep ignored files when updating dependencies", + help="delete ignored files when updating dependencies", ) # Options group From 6442a5754d3fe1112f6fc98ef7ac2d37b4d983eb Mon Sep 17 00:00:00 2001 From: Jace Browning Date: Wed, 31 Aug 2016 17:01:18 -0400 Subject: [PATCH 02/38] Fix repository URL for SSH clone example --- docs/setup/git.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/setup/git.md b/docs/setup/git.md index f6e9895a..067d840d 100644 --- a/docs/setup/git.md +++ b/docs/setup/git.md @@ -37,7 +37,7 @@ If you're using two-factory authentication on GitHub, you'll need to [provide a You can also set up SSH keys (for [GitHub](https://help.github.com/articles/generating-ssh-keys/)) and use a different URL: ```shell -$ git clone git://github.com//.git +$ git clone git@github.com:/.git ``` ## OAuth Tokens From e1d1bf0ca7309c0aa1c68a7a7ab17b096e4ea3a8 Mon Sep 17 00:00:00 2001 From: Jace Browning Date: Fri, 2 Sep 2016 22:39:25 -0400 Subject: [PATCH 03/38] Clarify terms in examples --- README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 928d75cb..2515fb39 100644 --- a/README.md +++ b/README.md @@ -72,9 +72,9 @@ $ gitman update which will essentially: -1. create a working tree at _root_/`location`/`name` +1. create a working tree at _(root)_/``/`` 2. fetch from `repo` and checkout the specified `rev` -3. symbolically link each `location`/`name` from _root_/`link` (if specified) +3. symbolically link each ``/`` from _(root)_/`` (if specified) 4. repeat for all nested working trees containing a configuration file 5. record the actual commit SHAs that were checked out (with `--lock` option) From 7a6c1b7e80c05804e7a09285c1b4c5a88cabe8ca Mon Sep 17 00:00:00 2001 From: Jace Browning Date: Mon, 26 Sep 2016 10:38:13 -0400 Subject: [PATCH 04/38] Update project structure --- .gitignore | 2 + .pep8rc | 3 +- .pylintrc | 6 ++ .travis.yml | 23 ++--- Makefile | 198 +++++++++++++++++++++------------------ README.md | 2 +- gitman/__main__.py | 2 +- gitman/tests/__init__.py | 2 +- requirements/ci.txt | 3 +- requirements/dev.txt | 6 +- scent.py | 13 +-- setup.py | 58 +++++++++--- tests/__init__.py | 2 +- 13 files changed, 185 insertions(+), 135 deletions(-) diff --git a/.gitignore b/.gitignore index a796a27e..ecba1b4e 100644 --- a/.gitignore +++ b/.gitignore @@ -37,10 +37,12 @@ Icon* .coverage .coverage.* /htmlcov +/xmlreport /pyunit.xml *.tmp # Build and release directories +*.spec /build /dist diff --git a/.pep8rc b/.pep8rc index 616e15b8..7be92bf2 100644 --- a/.pep8rc +++ b/.pep8rc @@ -1,6 +1,7 @@ [pep8] +# E402 module level import not at top of file (checked by PyLint) # E501: line too long (checked by PyLint) # E711: comparison to None (used to improve test style) # E712: comparison to True (used to improve test style) -ignore = E501,E711,E712 +ignore = E402,E501,E711,E712 diff --git a/.pylintrc b/.pylintrc index 1351a184..d3c37706 100644 --- a/.pylintrc +++ b/.pylintrc @@ -8,6 +8,12 @@ max-line-length=80 ignore-long-lines=^.*((https?:)|(pragma:)|(TODO:)).*$ +[SIMILARITIES] + +min-similarity-lines=4 + +ignore-imports=yes + [REPORTS] reports=no diff --git a/.travis.yml b/.travis.yml index d0e6c8f3..ca61ab75 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,32 +1,27 @@ -sudo: false - language: python python: -- 3.5 + - 3.5 cache: pip: true directories: - - env + - env env: global: - - RANDOM_SEED=12345 + - RANDOM_SEED=0 install: -- pip install coveralls scrutinizer-ocular - -before_script: -- make env -- make depends + - make install script: -- make check -- make test + - make check + - make test after_success: -- coveralls -- ocular + - pip install coveralls scrutinizer-ocular + - coveralls + - ocular notifications: email: diff --git a/Makefile b/Makefile index a912c29c..d254a0ea 100644 --- a/Makefile +++ b/Makefile @@ -2,8 +2,9 @@ PROJECT := GitMan PACKAGE := gitman REPOSITORY := jacebrowning/gitman -DIRECTORIES := $(PACKAGE) tests -FILES := setup.py $(shell find $(DIRECTORIES) -name '*.py') +PACKAGES := $(PACKAGE) tests +CONFIG := $(shell ls *.py) +MODULES := $(shell find $(PACKAGES) -name '*.py') $(CONFIG) # Python settings ifndef TRAVIS @@ -54,20 +55,8 @@ endif PYTHON := $(BIN_)python PIP := $(BIN_)pip EASY_INSTALL := $(BIN_)easy_install -RST2HTML := $(PYTHON) $(BIN_)rst2html.py -PDOC := $(PYTHON) $(BIN_)pdoc -MKDOCS := $(BIN_)mkdocs -PEP8 := $(BIN_)pep8 -PEP8RADIUS := $(BIN_)pep8radius -PEP257 := $(BIN_)pep257 -PYLINT := $(BIN_)pylint -PYREVERSE := $(BIN_)pyreverse -NOSE := $(BIN_)nosetests -PYTEST := $(BIN_)py.test -COVERAGE := $(BIN_)coverage -COVERAGE_SPACE := $(BIN_)coverage.space SNIFFER := $(BIN_)sniffer -HONCHO := PYTHONPATH=$(PWD) $(ACTIVATE) && $(BIN_)honcho +HONCHO := $(ACTIVATE) && $(BIN_)honcho # MAIN TASKS ################################################################### @@ -75,12 +64,16 @@ HONCHO := PYTHONPATH=$(PWD) $(ACTIVATE) && $(BIN_)honcho all: doc .PHONY: ci -ci: check test ## Run all targets that determine CI status +ci: check test ## Run all tasks that determine CI status .PHONY: watch -watch: depends .clean-test ## Continuously run all CI targets when files chanage +watch: install .clean-test ## Continuously run all CI tasks when files chanage $(SNIFFER) +.PHONY: run ## Start the program +run: install + $(PYTHON) $(PACKAGE)/__main__.py + # SYSTEM DEPENDENCIES ########################################################## .PHONY: doctor @@ -90,29 +83,19 @@ doctor: ## Confirm system dependencies are available # PROJECT DEPENDENCIES ######################################################### -DEPENDS := $(ENV)/.depends -DEPENDS_CI := $(ENV)/.depends-ci -DEPENDS_DEV := $(ENV)/.depends-dev - -env: $(PYTHON) - -$(PYTHON): - $(SYS_PYTHON) -m venv --clear $(ENV) - $(PYTHON) -m pip install --upgrade pip setuptools - -.PHONY: depends -depends: env $(DEPENDS) $(DEPENDS_CI) $(DEPENDS_DEV) ## Install all project dependnecies +DEPS_CI := $(ENV)/.install-ci +DEPS_DEV := $(ENV)/.install-dev +DEPS_BASE := $(ENV)/.install-base -$(DEPENDS): setup.py requirements.txt - $(PYTHON) setup.py develop - @ touch $@ # flag to indicate dependencies are installed +.PHONY: install +install: $(DEPS_CI) $(DEPS_DEV) $(DEPS_BASE) ## Install all project dependencies -$(DEPENDS_CI): requirements/ci.txt - $(PIP) install -r $^ +$(DEPS_CI): requirements/ci.txt $(PIP) + $(PIP) install --upgrade -r $< @ touch $@ # flag to indicate dependencies are installed -$(DEPENDS_DEV): requirements/dev.txt - $(PIP) install pip -r $^ +$(DEPS_DEV): requirements/dev.txt $(PIP) + $(PIP) install --upgrade -r $< ifdef WINDOWS @ echo "Manually install pywin32: https://sourceforge.net/projects/pywin32/files/pywin32" else ifdef MAC @@ -122,32 +105,52 @@ else ifdef LINUX endif @ touch $@ # flag to indicate dependencies are installed +$(DEPS_BASE): setup.py requirements.txt $(PYTHON) + $(PYTHON) setup.py develop + @ touch $@ # flag to indicate dependencies are installed + +$(PIP): $(PYTHON) + $(PYTHON) -m pip install --upgrade pip setuptools + @ touch $@ + +$(PYTHON): + $(SYS_PYTHON) -m venv $(ENV) + # CHECKS ####################################################################### +PEP8 := $(BIN_)pep8 +PEP8RADIUS := $(BIN_)pep8radius +PEP257 := $(BIN_)pep257 +PYLINT := $(BIN_)pylint + .PHONY: check -check: pep8 pep257 pylint ## Run all static analysis targets +check: pep8 pep257 pylint ## Run linters and static analysis .PHONY: pep8 -pep8: depends ## Check for convention issues - $(PEP8) $(DIRECTORIES) --config=.pep8rc +pep8: install ## Check for convention issues + $(PEP8) $(PACKAGES) $(CONFIG) --config=.pep8rc .PHONY: pep257 -pep257: depends ## Check for docstring issues - $(PEP257) $(DIRECTORIES) +pep257: install ## Check for docstring issues + $(PEP257) $(PACKAGES) $(CONFIG) .PHONY: pylint -pylint: depends ## Check for code issues - $(PYLINT) $(DIRECTORIES) --rcfile=.pylintrc +pylint: install ## Check for code issues + $(PYLINT) $(PACKAGES) $(CONFIG) --rcfile=.pylintrc .PHONY: fix -fix: depends +fix: install $(PEP8RADIUS) --docformatter --in-place # TESTS ######################################################################## +PYTEST := $(BIN_)py.test +COVERAGE := $(BIN_)coverage +COVERAGE_SPACE := $(BIN_)coverage.space + RANDOM_SEED ?= $(shell date +%s) -PYTEST_CORE_OPTS := --verbose -r xXw --maxfail=3 +PYTEST_CORE_OPTS := -r xXw -vv PYTEST_COV_OPTS := --cov=$(PACKAGE) --no-cov-on-fail --cov-report=term-missing --cov-report=html PYTEST_RANDOM_OPTS := --random --random-seed=$(RANDOM_SEED) @@ -155,40 +158,29 @@ PYTEST_OPTS := $(PYTEST_CORE_OPTS) $(PYTEST_COV_OPTS) $(PYTEST_RANDOM_OPTS) PYTEST_OPTS_FAILFAST := $(PYTEST_OPTS) --last-failed --exitfirst FAILURES := .cache/v/cache/lastfailed +REPORTS ?= xmlreport .PHONY: test test: test-all .PHONY: test-unit -test-unit: depends ## Run the unit tests +test-unit: install ## Run the unit tests @- mv $(FAILURES) $(FAILURES).bak - $(PYTEST) $(PYTEST_OPTS) $(PACKAGE) + $(PYTEST) $(PYTEST_OPTS) $(PACKAGE) --junitxml=$(REPORTS)/unit.xml @- mv $(FAILURES).bak $(FAILURES) -ifndef TRAVIS -ifndef APPVEYOR $(COVERAGE_SPACE) $(REPOSITORY) unit -endif -endif .PHONY: test-int -test-int: depends ## Run the integration tests +test-int: install ## Run the integration tests @ if test -e $(FAILURES); then $(PYTEST) $(PYTEST_OPTS_FAILFAST) tests; fi - $(PYTEST) $(PYTEST_OPTS) tests -ifndef TRAVIS -ifndef APPVEYOR + $(PYTEST) $(PYTEST_OPTS) tests --junitxml=$(REPORTS)/integration.xml $(COVERAGE_SPACE) $(REPOSITORY) integration -endif -endif .PHONY: test-all -test-all: depends ## Run all the tests - @ if test -e $(FAILURES); then $(PYTEST) $(PYTEST_OPTS_FAILFAST) $(DIRECTORIES); fi - $(PYTEST) $(PYTEST_OPTS) $(DIRECTORIES) -ifndef TRAVIS -ifndef APPVEYOR +test-all: install ## Run all the tests + @ if test -e $(FAILURES); then $(PYTEST) $(PYTEST_OPTS_FAILFAST) $(PACKAGES); fi + $(PYTEST) $(PYTEST_OPTS) $(PACKAGES) --junitxml=$(REPORTS)/overall.xml $(COVERAGE_SPACE) $(REPOSITORY) overall -endif -endif .PHONY: read-coverage read-coverage: @@ -196,27 +188,31 @@ read-coverage: # DOCUMENTATION ################################################################ +PYREVERSE := $(BIN_)pyreverse +PDOC := $(PYTHON) $(BIN_)pdoc +MKDOCS := $(BIN_)mkdocs + PDOC_INDEX := docs/apidocs/$(PACKAGE)/index.html MKDOCS_INDEX := site/index.html .PHONY: doc -doc: uml pdoc mkdocs ## Run all documentation targets +doc: uml pdoc mkdocs ## Run documentation generators .PHONY: uml -uml: depends docs/*.png ## Generate UML diagrams for classes and packages -docs/*.png: $(FILES) +uml: install docs/*.png ## Generate UML diagrams for classes and packages +docs/*.png: $(MODULES) $(PYREVERSE) $(PACKAGE) -p $(PACKAGE) -a 1 -f ALL -o png --ignore tests - mv -f classes_$(PACKAGE).png docs/classes.png - mv -f packages_$(PACKAGE).png docs/packages.png .PHONY: pdoc -pdoc: depends $(PDOC_INDEX) ## Generate API documentaiton with pdoc -$(PDOC_INDEX): $(FILES) +pdoc: install $(PDOC_INDEX) ## Generate API documentaiton with pdoc +$(PDOC_INDEX): $(MODULES) $(PDOC) --html --overwrite $(PACKAGE) --html-dir docs/apidocs @ touch $@ .PHONY: mkdocs -mkdocs: depends $(MKDOCS_INDEX) ## Build the documentation site with mkdocs +mkdocs: install $(MKDOCS_INDEX) ## Build the documentation site with mkdocs $(MKDOCS_INDEX): mkdocs.yml docs/*.md ln -sf `realpath README.md --relative-to=docs` docs/index.md ln -sf `realpath CHANGELOG.md --relative-to=docs/about` docs/about/changelog.md @@ -229,27 +225,48 @@ mkdocs-live: mkdocs ## Launch and continuously rebuild the mkdocs site eval "sleep 3; open http://127.0.0.1:8000" & $(MKDOCS) serve +# BUILD ######################################################################## + +PYINSTALLER := $(BIN_)pyinstaller +PYINSTALLER_MAKESPEC := $(BIN_)pyi-makespec + +DIST_FILES := dist/*.tar.gz dist/*.whl +EXE_FILES := dist/$(PROJECT).* + +.PHONY: dist +dist: install $(DIST_FILES) +$(DIST_FILES): $(MODULES) README.rst CHANGELOG.rst + rm -f $(DIST_FILES) + $(PYTHON) setup.py check --restructuredtext --strict --metadata + $(PYTHON) setup.py sdist + $(PYTHON) setup.py bdist_wheel + +%.rst: %.md + pandoc -f markdown_github -t rst -o $@ $< + +.PHONY: exe +exe: install $(EXE_FILES) +$(EXE_FILES): $(MODULES) $(PROJECT).spec + # For framework/shared support: https://github.com/yyuu/pyenv/wiki + $(PYINSTALLER) $(PROJECT).spec --noconfirm --clean + +$(PROJECT).spec: + $(PYINSTALLER_MAKESPEC) $(PACKAGE)/__main__.py --onefile --windowed --name=$(PROJECT) + # RELEASE ###################################################################### -.PHONY: register-test -register-test: README.rst CHANGELOG.rst ## Register the project on the test PyPI - $(PYTHON) setup.py register --strict --repository https://testpypi.python.org/pypi +TWINE := $(BIN_)twine .PHONY: register -register: README.rst CHANGELOG.rst ## Register the project on PyPI - $(PYTHON) setup.py register --strict - -.PHONY: upload-test -upload-test: register-test ## Upload the current version to the test PyPI - $(PYTHON) setup.py sdist upload --repository https://testpypi.python.org/pypi - $(PYTHON) setup.py bdist_wheel upload --repository https://testpypi.python.org/pypi - $(OPEN) https://testpypi.python.org/pypi/$(PROJECT) +register: dist ## Register the project on PyPI + @ echo NOTE: your project must be registered manually + @ echo https://github.com/pypa/python-packaging-user-guide/issues/263 + # TODO: switch to twine when the above issue is resolved + # $(TWINE) register dist/*.whl .PHONY: upload upload: .git-no-changes register ## Upload the current version to PyPI - $(PYTHON) setup.py check --restructuredtext --strict --metadata - $(PYTHON) setup.py sdist upload - $(PYTHON) setup.py bdist_wheel upload + $(TWINE) upload dist/* $(OPEN) https://pypi.python.org/pypi/$(PROJECT) .PHONY: .git-no-changes @@ -263,21 +280,18 @@ upload: .git-no-changes register ## Upload the current version to PyPI exit -1; \ fi; -%.rst: %.md - pandoc -f markdown_github -t rst -o $@ $< - # CLEANUP ###################################################################### .PHONY: clean -clean: .clean-dist .clean-test .clean-doc .clean-build +clean: .clean-dist .clean-test .clean-doc .clean-build ## Delete all generated and temporary files .PHONY: clean-all clean-all: clean .clean-env .clean-workspace .PHONY: .clean-build .clean-build: - find $(DIRECTORIES) -name '*.pyc' -delete - find $(DIRECTORIES) -name '__pycache__' -delete + find $(PACKAGES) -name '*.pyc' -delete + find $(PACKAGES) -name '__pycache__' -delete rm -rf *.egg-info .PHONY: .clean-doc @@ -286,11 +300,11 @@ clean-all: clean .clean-env .clean-workspace .PHONY: .clean-test .clean-test: - rm -rf .cache .pytest .coverage htmlcov + rm -rf .cache .pytest .coverage htmlcov xmlreport .PHONY: .clean-dist .clean-dist: - rm -rf dist build + rm -rf *.spec dist build .PHONY: .clean-env .clean-env: clean diff --git a/README.md b/README.md index 2515fb39..8b24b699 100644 --- a/README.md +++ b/README.md @@ -18,7 +18,7 @@ GitMan is a language-agnostic "dependency manager" using Git. It aims to serve a ## Installation -GitMan can be installed with pip: +Install GitMan with pip: ```sh $ pip install gitman diff --git a/gitman/__main__.py b/gitman/__main__.py index 1e771e17..4bf11366 100644 --- a/gitman/__main__.py +++ b/gitman/__main__.py @@ -1,6 +1,6 @@ """Package entry point.""" -from .cli import main +from gitman.cli import main if __name__ == '__main__': # pragma: no cover (manual test) diff --git a/gitman/tests/__init__.py b/gitman/tests/__init__.py index 73549fc5..0166504d 100644 --- a/gitman/tests/__init__.py +++ b/gitman/tests/__init__.py @@ -1,4 +1,4 @@ -"""Unit tests for the `gitman` package.""" +"""Unit tests for the package.""" def assert_calls(mock_call, expected): diff --git a/requirements/ci.txt b/requirements/ci.txt index fddd1f63..8fa33f18 100644 --- a/requirements/ci.txt +++ b/requirements/ci.txt @@ -9,9 +9,8 @@ pytest-describe pytest-expecter pytest-cov pytest-random -expecter freezegun # Coverage coverage -coverage.space +coverage.space >= 0.6.1 diff --git a/requirements/dev.txt b/requirements/dev.txt index fdf8f28c..857968fb 100644 --- a/requirements/dev.txt +++ b/requirements/dev.txt @@ -13,5 +13,9 @@ sniffer # Runner honcho -# Release +# Build wheel +pyinstaller + +# Release +twine diff --git a/scent.py b/scent.py index e1e1b071..0aaaf94a 100644 --- a/scent.py +++ b/scent.py @@ -1,4 +1,5 @@ """Configuration file for sniffer.""" +# pylint: disable=superfluous-parens,bad-continuation import os import time @@ -21,10 +22,10 @@ def python_files(filename): """Match Python source files.""" - return all( - (filename.endswith('.py'), - not os.path.basename(filename).startswith('.')), - ) + return all(( + filename.endswith('.py'), + not os.path.basename(filename).startswith('.'), + )) @runnable @@ -32,8 +33,8 @@ def python(*_): """Run targets for Python.""" for count, (command, title, retry) in enumerate(( - (('make', 'test-unit'), "Unit Tests", True), - (('make', 'test-int'), "Integration Tests", False), + (('make', 'test-unit', 'CI=true'), "Unit Tests", True), + (('make', 'test-int', 'CI=true'), "Integration Tests", False), (('make', 'test-all'), "Combined Tests", False), (('make', 'check'), "Static Analysis", True), (('make', 'doc'), None, True), diff --git a/setup.py b/setup.py index 1ff84cfd..8cb0f4f4 100644 --- a/setup.py +++ b/setup.py @@ -1,24 +1,52 @@ #!/usr/bin/env python -"""Setup script for GitMan.""" +"""Setup script for the package.""" + +import os +import sys import setuptools -from gitman import __project__, __version__, CLI, PLUGIN, DESCRIPTION -try: - README = open("README.rst").read() - CHANGELOG = open("CHANGELOG.rst").read() -except IOError: - LONG_DESCRIPTION = "" -else: - LONG_DESCRIPTION = README + '\n' + CHANGELOG +PACKAGE_NAME = 'gitman' +MINIMUM_PYTHON_VERSION = 3, 5 + + +def check_python_version(): + """Exit when the Python version is too low.""" + if sys.version_info < MINIMUM_PYTHON_VERSION: + sys.exit("Python {}.{}+ is required.".format(*MINIMUM_PYTHON_VERSION)) + + +def read_package_variable(key): + """Read the value of a variable from the package without importing.""" + module_path = os.path.join(PACKAGE_NAME, '__init__.py') + with open(module_path) as module: + for line in module: + parts = line.strip().split(' ') + if parts and parts[0] == key: + return parts[-1].strip("'") + assert 0, "'{0}' not found in '{1}'".format(key, module_path) + + +def read_descriptions(): + """Build a description for the project from documentation files.""" + try: + readme = open("README.rst").read() + changelog = open("CHANGELOG.rst").read() + except IOError: + return "" + else: + return readme + '\n' + changelog + + +check_python_version() setuptools.setup( - name=__project__, - version=__version__, + name=read_package_variable('__project__'), + version=read_package_variable('__version__'), - description=DESCRIPTION, + description=read_package_variable('DESCRIPTION'), url='https://jacebrowning/gitman', author='Jace Browning', author_email='jacebrowning@gmail.com', @@ -26,13 +54,13 @@ packages=setuptools.find_packages(), entry_points={'console_scripts': [ - CLI + ' = gitman.cli:main', - 'git-' + PLUGIN + ' = gitman.plugin:main', + read_package_variable('CLI') + ' = gitman.cli:main', + 'git-' + read_package_variable('PLUGIN') + ' = gitman.plugin:main', # Legacy entry points: 'gdm = gitman.cli:main', ]}, - long_description=LONG_DESCRIPTION, + long_description=read_descriptions(), license='MIT', classifiers=[ 'Development Status :: 5 - Production/Stable', diff --git a/tests/__init__.py b/tests/__init__.py index cab63056..a916f18f 100644 --- a/tests/__init__.py +++ b/tests/__init__.py @@ -1 +1 @@ -"""Integration tests for the `gitman` package.""" +"""Integration tests for the package.""" From ed343d797f20181c0ae6cb4ba22ef6677a5adeec Mon Sep 17 00:00:00 2001 From: Jace Browning Date: Sat, 22 Oct 2016 23:27:17 -0400 Subject: [PATCH 05/38] Update project from the template --- .travis.yml | 3 + .verchew | 34 +++++++ CONTRIBUTING.md | 28 +++--- Makefile | 5 +- bin/verchew | 208 +++++++++++++++++++++++++++++++++++++++++++ requirements/ci.txt | 2 +- requirements/dev.txt | 2 +- tests/conftest.py | 10 +-- 8 files changed, 267 insertions(+), 25 deletions(-) create mode 100644 .verchew create mode 100755 bin/verchew diff --git a/.travis.yml b/.travis.yml index ca61ab75..d598597b 100644 --- a/.travis.yml +++ b/.travis.yml @@ -11,6 +11,9 @@ env: global: - RANDOM_SEED=0 +before_install: + - make doctor || echo "Some system dependencies might be missing" + install: - make install diff --git a/.verchew b/.verchew new file mode 100644 index 00000000..c2a9a536 --- /dev/null +++ b/.verchew @@ -0,0 +1,34 @@ +[Make] + +cli = make + +version = GNU Make + + +[Python] + +cli = python + +version = Python 3.5. + + +[virtualenv] + +cli = virtualenv + +version = 15. + + +[pandoc] + +cli = pandoc + +version = 1. + + +[Graphviz] + +cli = dot +cli_version_arg = -V + +version = 2. diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 01994fb0..ec01c221 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -5,18 +5,24 @@ ### Requirements * Make: - * Windows: http://cygwin.com/install.html + * Windows: https://cygwin.com/install.html * Mac: https://developer.apple.com/xcode - * Linux: http://www.gnu.org/software/make (likely already installed) + * Linux: https://www.gnu.org/software/make (likely already installed) * Pandoc: http://johnmacfarlane.net/pandoc/installing.html * Graphviz: http://www.graphviz.org/Download.php +To confirm these system dependencies are configured correctly: + +```sh +$ make doctor +``` + ### Installation -Create a virtual environment: +Install project dependencies into a virtual environment: -``` -$ make env +```sh +$ make install ``` ## Development Tasks @@ -25,14 +31,14 @@ $ make env Manually run the tests: -``` +```sh $ make test $ make tests # includes integration tests ``` or keep them running on change: -``` +```sh $ make watch ``` @@ -42,7 +48,7 @@ $ make watch Build the documentation: -``` +```sh $ make doc ``` @@ -50,7 +56,7 @@ $ make doc Run linters and static analyzers: -``` +```sh $ make pep8 $ make pep257 $ make pylint @@ -61,7 +67,7 @@ $ make check # includes all checks The CI server will report overall build status: -``` +```sh $ make ci ``` @@ -69,7 +75,7 @@ $ make ci Release to PyPI: -``` +```sh $ make upload-test # dry run upload to a test server $ make upload ``` diff --git a/Makefile b/Makefile index d254a0ea..61c7494d 100644 --- a/Makefile +++ b/Makefile @@ -78,8 +78,7 @@ run: install .PHONY: doctor doctor: ## Confirm system dependencies are available - @ echo "Checking Python version:" - @ python --version | tee /dev/stderr | grep -q "3.5." + bin/verchew # PROJECT DEPENDENCIES ######################################################### @@ -114,7 +113,7 @@ $(PIP): $(PYTHON) @ touch $@ $(PYTHON): - $(SYS_PYTHON) -m venv $(ENV) + $(SYS_PYTHON) -m venv --clear $(ENV) # CHECKS ####################################################################### diff --git a/bin/verchew b/bin/verchew new file mode 100755 index 00000000..6771b1ec --- /dev/null +++ b/bin/verchew @@ -0,0 +1,208 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- + +# The MIT License (MIT) +# Copyright © 2016, Jace Browning +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in +# all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +# SOFTWARE. + + +from __future__ import unicode_literals + +import os +import sys +import argparse +try: + import configparser # Python 3 +except ImportError: + import ConfigParser as configparser # Python 2 +from collections import OrderedDict +from subprocess import Popen, PIPE, STDOUT +import logging + +__version__ = '0.4' + +PY2 = sys.version_info[0] == 2 +CONFIG_FILENAMES = ['.verchew', '.verchewrc', 'verchew.ini', '.verchew.ini'] +STYLE = { + "x": "✘", + "~": "✔" +} +COLOR = { + "x": "\033[91m", # red + "~": "\033[92m", # green + None: "\033[0m", # reset +} + +log = logging.getLogger(__name__) + + +def main(): + args = parse_args() + configure_logging(args.verbose) + + log.debug("PWD: %s", os.getenv('PWD')) + log.debug("PATH: %s", os.getenv('PATH')) + + path = find_config(args.root) + config = parse_config(path) + + if not check_dependencies(config): + sys.exit(1) + + +def parse_args(): + parser = argparse.ArgumentParser() + + version = "%(prog)s v" + __version__ + parser.add_argument('--version', action='version', version=version) + parser.add_argument('-v', '--verbose', action='count', default=0, + help="enable verbose logging") + parser.add_argument('-r', '--root', metavar='PATH', + help="use a custom project root") + + args = parser.parse_args() + + return args + + +def configure_logging(count=0): + if count == 0: + level = logging.WARNING + elif count == 1: + level = logging.INFO + else: + level = logging.DEBUG + + logging.basicConfig(level=level, format="%(levelname)s: %(message)s") + + +def find_config(root=None, config_filenames=None): + root = root or os.getcwd() + config_filenames = config_filenames or CONFIG_FILENAMES + + path = None + log.info("Looking for config file in: %s", root) + log.debug("Filename options: %s", ", ".join(config_filenames)) + for filename in os.listdir(root): + if filename in config_filenames: + path = os.path.join(root, filename) + log.info("Found config file: %s", path) + return path + + msg = "No config file found in: {0}".format(root) + raise RuntimeError(msg) + + +def parse_config(path): + data = OrderedDict() + + log.info("Parsing config file: %s", path) + config = configparser.ConfigParser() + config.read(path) + + for section in config.sections(): + data[section] = OrderedDict() + for name, value in config.items(section): + data[section][name] = value + + return data + + +def check_dependencies(config): + success = [] + + for name, settings in config.items(): + show("Checking for {0}...".format(name), head=True) + output = get_version(settings['cli'], settings.get('cli_version_arg')) + if match_version(settings['version'], output): + show(_("~") + " MATCHED: {0}".format(settings['version'])) + success.append(_("~")) + else: + show(_("x") + " EXPECTED: {0}".format(settings['version'])) + success.append(_("x")) + + show("Results: " + " ".join(success), head=True) + + return _("x") not in success + + +def get_version(program, argument=None): + argument = argument or '--version' + args = [program, argument] + + show("$ {0}".format(" ".join(args))) + output = call(args) + show(output.splitlines()[0]) + + return output + + +def match_version(pattern, output): + return output.startswith(pattern) or " " + pattern in output + + +def call(args): + try: + process = Popen(args, stdout=PIPE, stderr=STDOUT) + except OSError: + log.debug("Command not found: %s", args[0]) + output = "sh: command not found: {0}".format(args[0]) + else: + raw = process.communicate()[0] + output = raw.decode('utf-8').strip() + log.debug("Command output: %r", output) + + return output + + +def show(text, start='', end='\n', head=False): + """Python 2 and 3 compatible version of print.""" + if head: + start = '\n' + end = '\n\n' + + if log.getEffectiveLevel() < logging.WARNING: + log.info(text) + else: + formatted = (start + text + end) + if PY2: + formatted = formatted.encode('utf-8') + sys.stdout.write(formatted) + sys.stdout.flush() + + +def _(word, utf8=None, tty=None): + """Format and colorize a word based on available encoding.""" + formatted = word + + style_support = sys.stdout.encoding == 'UTF-8' if utf8 is None else utf8 + color_support = sys.stdout.isatty() if tty is None else tty + + if style_support: + formatted = STYLE.get(word, word) + + if color_support and COLOR.get(word): + formatted = COLOR[word] + formatted + COLOR[None] + + return formatted + + +if __name__ == '__main__': # pragma: no cover + main() diff --git a/requirements/ci.txt b/requirements/ci.txt index 8fa33f18..86a0630c 100644 --- a/requirements/ci.txt +++ b/requirements/ci.txt @@ -13,4 +13,4 @@ freezegun # Coverage coverage -coverage.space >= 0.6.1 +coverage.space >= 0.6.2 diff --git a/requirements/dev.txt b/requirements/dev.txt index 857968fb..c426f7dd 100644 --- a/requirements/dev.txt +++ b/requirements/dev.txt @@ -8,7 +8,7 @@ pygments # Tooling pep8radius -sniffer +sniffer >= 0.3.6 # Runner honcho diff --git a/tests/conftest.py b/tests/conftest.py index 0e4f9162..9f6a9740 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -1,18 +1,10 @@ """Integration tests configuration.""" -# pylint: disable=unused-argument - -import os import yorm from gitman.tests.conftest import pytest_configure # pylint: disable=unused-import -# TODO: delete if unused (and files) -ROOT = os.path.dirname(__file__) -FILES = os.path.join(ROOT, 'files') - - -def pytest_runtest_setup(item): +def pytest_runtest_setup(item): # pylint: disable=unused-argument """Ensure files are created for integration tests.""" yorm.settings.fake = False From 43dd74dae79f193d44c8c03250354b7272fad248 Mon Sep 17 00:00:00 2001 From: Jace Browning Date: Sat, 22 Oct 2016 23:29:26 -0400 Subject: [PATCH 06/38] Update recommended Git version --- .verchew | 7 +++++++ README.md | 2 +- 2 files changed, 8 insertions(+), 1 deletion(-) diff --git a/.verchew b/.verchew index c2a9a536..8e919925 100644 --- a/.verchew +++ b/.verchew @@ -12,6 +12,13 @@ cli = python version = Python 3.5. +[Git] + +cli = git + +version = 2. + + [virtualenv] cli = virtualenv diff --git a/README.md b/README.md index 8b24b699..d049675b 100644 --- a/README.md +++ b/README.md @@ -13,7 +13,7 @@ GitMan is a language-agnostic "dependency manager" using Git. It aims to serve a ## Requirements * Python 3.5+ -* Git 1.8+ (with [stored credentials](http://git-dependency-manager.info/setup/git/)) +* Git 2.8+ (with [stored credentials](http://git-dependency-manager.info/setup/git/)) * Unix shell (or Cygwin/MinGW/etc. on Windows) ## Installation From 9949ba4cce277802bb1d2706b0b43e677a259361 Mon Sep 17 00:00:00 2001 From: Jace Browning Date: Sat, 22 Oct 2016 23:30:23 -0400 Subject: [PATCH 07/38] Update documentation link --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index d049675b..15e1e97e 100644 --- a/README.md +++ b/README.md @@ -13,7 +13,7 @@ GitMan is a language-agnostic "dependency manager" using Git. It aims to serve a ## Requirements * Python 3.5+ -* Git 2.8+ (with [stored credentials](http://git-dependency-manager.info/setup/git/)) +* Git 2.8+ (with [stored credentials](http://gitman.readthedocs.io/en/latest/setup/git/)) * Unix shell (or Cygwin/MinGW/etc. on Windows) ## Installation From 003a7e5c8a5c20ff2d1efaa9266588a6261c437a Mon Sep 17 00:00:00 2001 From: Daniel Brosche Date: Tue, 22 Nov 2016 13:02:57 +0100 Subject: [PATCH 08/38] Added package self declaration to improve debugging support --- gitman/__main__.py | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/gitman/__main__.py b/gitman/__main__.py index 4bf11366..46b3eb38 100644 --- a/gitman/__main__.py +++ b/gitman/__main__.py @@ -1,5 +1,13 @@ """Package entry point.""" +# Declare itself as package if needed +if __name__ == '__main__' and __package__ is None: + import os, sys, importlib + parent_dir = os.path.abspath(os.path.dirname(__file__)) + sys.path.append(os.path.dirname(parent_dir)) + __package__ = os.path.basename(parent_dir) + importlib.import_module(__package__) + from gitman.cli import main From 24017fbb2ae146c10c42fb9c2eab83d8f68ffb50 Mon Sep 17 00:00:00 2001 From: Jace Browning Date: Thu, 24 Nov 2016 13:16:50 -0500 Subject: [PATCH 09/38] Disable import checks in __main__ --- .pep8rc | 3 ++- gitman/__main__.py | 8 +++++--- 2 files changed, 7 insertions(+), 4 deletions(-) diff --git a/.pep8rc b/.pep8rc index 7be92bf2..8baded3e 100644 --- a/.pep8rc +++ b/.pep8rc @@ -1,7 +1,8 @@ [pep8] +# E401 multiple imports on one line (checked by PyLint) # E402 module level import not at top of file (checked by PyLint) # E501: line too long (checked by PyLint) # E711: comparison to None (used to improve test style) # E712: comparison to True (used to improve test style) -ignore = E402,E501,E711,E712 +ignore = E401,E402,E501,E711,E712 diff --git a/gitman/__main__.py b/gitman/__main__.py index 46b3eb38..8663f2e4 100644 --- a/gitman/__main__.py +++ b/gitman/__main__.py @@ -1,15 +1,17 @@ """Package entry point.""" -# Declare itself as package if needed -if __name__ == '__main__' and __package__ is None: +# Declare itself as package if needed for better debugging support +# pylint: disable=multiple-imports,wrong-import-position,redefined-builtin +if __name__ == '__main__' and __package__ is None: # pragma: no cover import os, sys, importlib parent_dir = os.path.abspath(os.path.dirname(__file__)) sys.path.append(os.path.dirname(parent_dir)) __package__ = os.path.basename(parent_dir) importlib.import_module(__package__) + from gitman.cli import main -if __name__ == '__main__': # pragma: no cover (manual test) +if __name__ == '__main__': # pragma: no cover main() From 9f66f67297e8c519123fa5c4b51a0cece111a4b9 Mon Sep 17 00:00:00 2001 From: Jace Browning Date: Mon, 28 Nov 2016 14:08:39 -0500 Subject: [PATCH 10/38] Move example file to the root --- .gitignore | 1 + tests/files/gdm.yml => gitman.yml | 8 ++++---- 2 files changed, 5 insertions(+), 4 deletions(-) rename tests/files/gdm.yml => gitman.yml (77%) diff --git a/.gitignore b/.gitignore index ecba1b4e..22836e5b 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,4 @@ +/demo */tests/files/gitman_sources # Temporary Python files diff --git a/tests/files/gdm.yml b/gitman.yml similarity index 77% rename from tests/files/gdm.yml rename to gitman.yml index cc378c39..a39d29f6 100644 --- a/tests/files/gdm.yml +++ b/gitman.yml @@ -1,4 +1,4 @@ -location: /tmp/gitman-test-dependencies +location: demo sources: - name: gitman_1 link: '' @@ -11,12 +11,12 @@ sources: - name: gitman_3 link: '' repo: https://github.com/jacebrowning/gitman-demo - rev: master@{2015-06-16 10:30:30} + rev: master@{2015-06-18 11:11:11} sources_locked: - name: gitman_1 link: '' repo: https://github.com/jacebrowning/gitman-demo - rev: eb37743011a398b208dd9f9ef79a408c0fc10d48 + rev: 1de84ca1d315f81b035cd7b0ecf87ca2025cdacd - name: gitman_2 link: '' repo: https://github.com/jacebrowning/gitman-demo @@ -24,4 +24,4 @@ sources_locked: - name: gitman_3 link: '' repo: https://github.com/jacebrowning/gitman-demo - rev: 9bf18e16b956041f0267c21baad555a23237b52e + rev: 2da24fca34af3748e3cab61db81a2ae8b35aec94 From 187455643db76eea59779444564bbaceef4837cc Mon Sep 17 00:00:00 2001 From: Jace Browning Date: Mon, 5 Dec 2016 21:33:39 -0500 Subject: [PATCH 11/38] Use the existing virtual environment on Travis CI --- .travis.yml | 2 +- Makefile | 45 +++++++++++++++++++++++---------------------- 2 files changed, 24 insertions(+), 23 deletions(-) diff --git a/.travis.yml b/.travis.yml index d598597b..d52d9b55 100644 --- a/.travis.yml +++ b/.travis.yml @@ -5,7 +5,7 @@ python: cache: pip: true directories: - - env + - ~/virtualenv env: global: diff --git a/Makefile b/Makefile index 61c7494d..8361238b 100644 --- a/Makefile +++ b/Makefile @@ -33,7 +33,11 @@ else endif # Virtual environment paths -ENV := env +ifdef TRAVIS + ENV := $(shell dirname $(shell dirname $(shell which $(SYS_PYTHON))))/ +else + ENV := env +endif ifneq ($(findstring win32, $(PLATFORM)), ) BIN := $(ENV)/Scripts ACTIVATE := $(BIN)/activate.bat @@ -49,14 +53,11 @@ else endif # Virtual environment executables -ifndef TRAVIS - BIN_ := $(BIN)/ -endif -PYTHON := $(BIN_)python -PIP := $(BIN_)pip -EASY_INSTALL := $(BIN_)easy_install -SNIFFER := $(BIN_)sniffer -HONCHO := $(ACTIVATE) && $(BIN_)honcho +PYTHON := $(BIN)/python +PIP := $(BIN)/pip +EASY_INSTALL := $(BIN)/easy_install +SNIFFER := $(BIN)/sniffer +HONCHO := $(ACTIVATE) && $(BIN)/honcho # MAIN TASKS ################################################################### @@ -117,10 +118,10 @@ $(PYTHON): # CHECKS ####################################################################### -PEP8 := $(BIN_)pep8 -PEP8RADIUS := $(BIN_)pep8radius -PEP257 := $(BIN_)pep257 -PYLINT := $(BIN_)pylint +PEP8 := $(BIN)/pep8 +PEP8RADIUS := $(BIN)/pep8radius +PEP257 := $(BIN)/pep257 +PYLINT := $(BIN)/pylint .PHONY: check check: pep8 pep257 pylint ## Run linters and static analysis @@ -143,9 +144,9 @@ fix: install # TESTS ######################################################################## -PYTEST := $(BIN_)py.test -COVERAGE := $(BIN_)coverage -COVERAGE_SPACE := $(BIN_)coverage.space +PYTEST := $(BIN)/py.test +COVERAGE := $(BIN)/coverage +COVERAGE_SPACE := $(BIN)/coverage.space RANDOM_SEED ?= $(shell date +%s) @@ -187,9 +188,9 @@ read-coverage: # DOCUMENTATION ################################################################ -PYREVERSE := $(BIN_)pyreverse -PDOC := $(PYTHON) $(BIN_)pdoc -MKDOCS := $(BIN_)mkdocs +PYREVERSE := $(BIN)/pyreverse +PDOC := $(PYTHON) $(BIN)/pdoc +MKDOCS := $(BIN)/mkdocs PDOC_INDEX := docs/apidocs/$(PACKAGE)/index.html MKDOCS_INDEX := site/index.html @@ -226,8 +227,8 @@ mkdocs-live: mkdocs ## Launch and continuously rebuild the mkdocs site # BUILD ######################################################################## -PYINSTALLER := $(BIN_)pyinstaller -PYINSTALLER_MAKESPEC := $(BIN_)pyi-makespec +PYINSTALLER := $(BIN)/pyinstaller +PYINSTALLER_MAKESPEC := $(BIN)/pyi-makespec DIST_FILES := dist/*.tar.gz dist/*.whl EXE_FILES := dist/$(PROJECT).* @@ -254,7 +255,7 @@ $(PROJECT).spec: # RELEASE ###################################################################### -TWINE := $(BIN_)twine +TWINE := $(BIN)/twine .PHONY: register register: dist ## Register the project on PyPI From d79233b7780bc41abc611b237c385cc5c9b8a76b Mon Sep 17 00:00:00 2001 From: Jace Browning Date: Mon, 5 Dec 2016 21:51:15 -0500 Subject: [PATCH 12/38] Update verchew to 0.5 --- .travis.yml | 2 +- bin/verchew | 8 +++++--- 2 files changed, 6 insertions(+), 4 deletions(-) diff --git a/.travis.yml b/.travis.yml index d52d9b55..37e7ce54 100644 --- a/.travis.yml +++ b/.travis.yml @@ -12,7 +12,7 @@ env: - RANDOM_SEED=0 before_install: - - make doctor || echo "Some system dependencies might be missing" + - make doctor install: - make install diff --git a/bin/verchew b/bin/verchew index 6771b1ec..2675c445 100755 --- a/bin/verchew +++ b/bin/verchew @@ -36,7 +36,7 @@ from collections import OrderedDict from subprocess import Popen, PIPE, STDOUT import logging -__version__ = '0.4' +__version__ = '0.5' PY2 = sys.version_info[0] == 2 CONFIG_FILENAMES = ['.verchew', '.verchewrc', 'verchew.ini', '.verchew.ini'] @@ -63,7 +63,7 @@ def main(): path = find_config(args.root) config = parse_config(path) - if not check_dependencies(config): + if not check_dependencies(config) and args.exit_code: sys.exit(1) @@ -75,7 +75,9 @@ def parse_args(): parser.add_argument('-v', '--verbose', action='count', default=0, help="enable verbose logging") parser.add_argument('-r', '--root', metavar='PATH', - help="use a custom project root") + help="specify a custom project root directory") + parser.add_argument('--exit-code', action='store_true', + help="return a non-zero exit code on failure") args = parser.parse_args() From 4054252488bd74e66bff341026fbd657a8cb8e7c Mon Sep 17 00:00:00 2001 From: Jace Browning Date: Tue, 6 Dec 2016 08:45:08 -0500 Subject: [PATCH 13/38] Only cache the main Python virtual environment --- .travis.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.travis.yml b/.travis.yml index 37e7ce54..66383a79 100644 --- a/.travis.yml +++ b/.travis.yml @@ -5,7 +5,7 @@ python: cache: pip: true directories: - - ~/virtualenv + - ~/virtualenv/python3.5.* env: global: From 52e4fb1b4e5dd4b98122c78eaa00abee4286d4de Mon Sep 17 00:00:00 2001 From: Jace Browning Date: Thu, 5 Jan 2017 15:03:53 -0500 Subject: [PATCH 14/38] Update docs to recommend using a vendor subdirectory --- README.md | 8 ++++---- docs/use-cases/branch-tracking.md | 4 ++-- docs/use-cases/build-integration.md | 2 +- docs/use-cases/linked-features.md | 2 +- 4 files changed, 8 insertions(+), 8 deletions(-) diff --git a/README.md b/README.md index 15e1e97e..718fcb88 100644 --- a/README.md +++ b/README.md @@ -37,7 +37,7 @@ $ python setup.py install Create a configuration file (`gitman.yml` or `.gitman.yml`) in the root of your working tree: ```yaml -location: vendor +location: vendor/gitman sources: - name: framework repo: https://github.com/kstenerud/iOS-Universal-Framework @@ -51,7 +51,7 @@ sources: Ignore the dependency storage location: ```sh -$ echo vendor >> .gitignore +$ echo vendor/gitman >> .gitignore ``` # Usage @@ -72,9 +72,9 @@ $ gitman update which will essentially: -1. create a working tree at _(root)_/``/`` +1. create a working tree at ``/``/`` 2. fetch from `repo` and checkout the specified `rev` -3. symbolically link each ``/`` from _(root)_/`` (if specified) +3. symbolically link each ``/`` from ``/`` (if specified) 4. repeat for all nested working trees containing a configuration file 5. record the actual commit SHAs that were checked out (with `--lock` option) diff --git a/docs/use-cases/branch-tracking.md b/docs/use-cases/branch-tracking.md index c846890d..afac2bf9 100644 --- a/docs/use-cases/branch-tracking.md +++ b/docs/use-cases/branch-tracking.md @@ -7,7 +7,7 @@ One common use case of `gitman` is to track versions of related product sub-comp A web app's `gitman.yml` might look something like: ```yaml -location: vendor +location: vendor/gitman sources: - name: api repo: https://github.com/example/api @@ -25,7 +25,7 @@ package.json node_modules gitman.yml -vendor/api # dependency @ b27308 +vendor/gitman/api # dependency @ b27308 app tests diff --git a/docs/use-cases/build-integration.md b/docs/use-cases/build-integration.md index 3eda49ea..15ce12fd 100644 --- a/docs/use-cases/build-integration.md +++ b/docs/use-cases/build-integration.md @@ -26,7 +26,7 @@ clean: using a configuration file similar to: ```yaml -location: vendor +location: vendor/gitman sources: - name: lib_foo repo: https://github.com/example/lib_foo diff --git a/docs/use-cases/linked-features.md b/docs/use-cases/linked-features.md index 78fcdb2a..0b252966 100644 --- a/docs/use-cases/linked-features.md +++ b/docs/use-cases/linked-features.md @@ -8,7 +8,7 @@ Another use case of `gitman` is to test experimental versions of related product By manually modifying the `sources_locked` section, a particular version of the API can be checked out to help finish the complete feature in the web app: ```yaml -location: vendor +location: vendor/gitman sources: - name: api repo: https://github.com/example/api From 3b2ca4b6e1aa0f4adb17b48f0899fbc38b44d61b Mon Sep 17 00:00:00 2001 From: nomorgan Date: Thu, 5 Jan 2017 21:44:25 +0100 Subject: [PATCH 15/38] #132 -- preliminary windows support --- gitman/shell.py | 22 +++++++++++++++------- 1 file changed, 15 insertions(+), 7 deletions(-) diff --git a/gitman/shell.py b/gitman/shell.py index da890ece..a964c454 100644 --- a/gitman/shell.py +++ b/gitman/shell.py @@ -3,6 +3,7 @@ import os import subprocess import logging +import shutil from . import common from .exceptions import ShellError @@ -57,19 +58,26 @@ def call(name, *args, _show=True, _ignore=False): def mkdir(path): - call('mkdir', '-p', path) + os.mkdir(path) + # call('mkdir', '-p', path) def cd(path, _show=True): - call('cd', path, _show=_show) + os.chdir(path) + #call('cd', path, _show=_show) def ln(source, target): - dirpath = os.path.dirname(target) - if not os.path.isdir(dirpath): - mkdir(dirpath) - call('ln', '-s', source, target) + if not os.name=='nt': + dirpath = os.path.dirname(target) + if not os.path.isdir(dirpath): + mkdir(dirpath) + call('ln', '-s', source, target) def rm(path): - call('rm', '-rf', path) + if not os.path.isdir(dirpath): + os.remove(path) + else: + shutil.rmtree(path) + #call('rm', '-rf', path) From 85b516703aab102f851e697d70acb4265012be47 Mon Sep 17 00:00:00 2001 From: Jace Browning Date: Thu, 5 Jan 2017 18:12:15 -0500 Subject: [PATCH 16/38] Add color to the output Closes #125 --- CHANGELOG.md | 4 ++++ gitman/__init__.py | 2 +- gitman/cli.py | 27 +++++++++++++---------- gitman/commands.py | 24 +++++++++++++------- gitman/common.py | 49 ++++++++++++++++++++++++++++++++++++++--- gitman/git.py | 2 +- gitman/models/source.py | 21 +++++++++--------- gitman/shell.py | 2 +- tests/test_cli.py | 6 +++-- 9 files changed, 99 insertions(+), 38 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index b95cf0c2..59af5cf4 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,9 @@ # Revision History +## 1.1 (unreleased) + +- Added coloring to the command-line output. + ## 1.0.2 (2016/07/28) - Moved documentation to http://gitman.readthedocs.io/. diff --git a/gitman/__init__.py b/gitman/__init__.py index 17bfc8d1..145c6c09 100644 --- a/gitman/__init__.py +++ b/gitman/__init__.py @@ -3,7 +3,7 @@ import sys __project__ = 'GitMan' -__version__ = '1.0.2' +__version__ = '1.1b1' CLI = 'gitman' PLUGIN = 'deps' diff --git a/gitman/cli.py b/gitman/cli.py index c7c55068..6505c6df 100644 --- a/gitman/cli.py +++ b/gitman/cli.py @@ -115,17 +115,17 @@ def main(args=None, function=None): common.configure_logging(namespace.verbose) # Run the program - function, args, kwargs, exit_msg = _get_command(function, namespace) + function, args, kwargs, exit_message = _get_command(function, namespace) if function is None: parser.print_help() sys.exit(1) - _run_command(function, args, kwargs, exit_msg) + _run_command(function, args, kwargs, exit_message) def _get_command(function, namespace): args = [] kwargs = {} - exit_msg = "" + exit_message = None if namespace.command in ['install', 'update']: function = getattr(commands, namespace.command) @@ -139,7 +139,7 @@ def _get_command(function, namespace): if namespace.command == 'update': kwargs.update(recurse=namespace.recurse, lock=namespace.lock) - exit_msg = "\n" + "Run again with '--force' to overwrite" + exit_message = "Run again with '--force' to overwrite changes" elif namespace.command == 'list': function = commands.display @@ -156,7 +156,7 @@ def _get_command(function, namespace): function = commands.delete kwargs.update(root=namespace.root, force=namespace.force) - exit_msg = "\n" + "Run again with '--force' to ignore" + exit_message = "Run again with '--force' to ignore changes" elif namespace.command == 'show': function = commands.show @@ -171,26 +171,29 @@ def _get_command(function, namespace): function = commands.edit kwargs.update(root=namespace.root) - return function, args, kwargs, exit_msg + return function, args, kwargs, exit_message -def _run_command(function, args, kwargs, exit_msg): +def _run_command(function, args, kwargs, exit_message): success = False try: log.debug("Running %s command...", getattr(function, '__name__', 'a')) success = function(*args, **kwargs) except KeyboardInterrupt: log.debug("Command canceled") - exit_msg = "" except RuntimeError as exc: - exit_msg = str(exc) + exit_msg - else: - exit_msg = "" + common.dedent(0) + common.show(str(exc), color='error') + common.show() + if exit_message: + common.show(exit_message, color='message') + common.show() + if success: log.debug("Command succeeded") else: log.debug("Command failed") - sys.exit(exit_msg or 1) + sys.exit(1) if __name__ == '__main__': # pragma: no cover (manual test) diff --git a/gitman/commands.py b/gitman/commands.py index 7dce45e0..6ef70a34 100644 --- a/gitman/commands.py +++ b/gitman/commands.py @@ -45,7 +45,8 @@ def install(*names, root=None, depth=None, config = load_config(root) if config: - common.show("Installing dependencies...", log=False) + common.show() + common.show("Installing dependencies...", color='message', log=False) common.show() count = config.install_deps(*names, update=False, depth=depth, force=force, fetch=fetch, clean=clean) @@ -79,14 +80,16 @@ def update(*names, root=None, depth=None, config = load_config(root) if config: - common.show("Updating dependencies...", log=False) + common.show() + common.show("Updating dependencies...", color='message', log=False) common.show() count = config.install_deps( *names, update=True, depth=depth, recurse=recurse, force=force, fetch=True, clean=clean) common.dedent(level=0) if count and lock is not False: - common.show("Recording installed versions...", log=False) + common.show("Recording installed versions...", + color='message', log=False) common.show() config.lock_deps(*names, obey_existing=lock is None) @@ -111,7 +114,9 @@ def display(*, root=None, depth=None, allow_dirty=True): config = load_config(root) if config: - common.show("Displaying current dependency versions...", log=False) + common.show() + common.show("Displaying current dependency versions...", + color='message', log=False) common.show() config.log(datetime.datetime.now().strftime("%F %T")) count = 0 @@ -140,7 +145,8 @@ def lock(*names, root=None): config = load_config(root) if config: - common.show("Locking dependencies...", log=False) + common.show() + common.show("Locking dependencies...", color='message', log=False) common.show() count = config.lock_deps(*names, obey_existing=False) common.dedent(level=0) @@ -165,11 +171,13 @@ def delete(*, root=None, force=False): config = load_config(root) if config: - common.show("Checking for uncommitted changes...", log=False) + common.show() + common.show("Checking for uncommitted changes...", + color='message', log=False) common.show() count = len(list(config.get_deps(allow_dirty=force))) common.dedent(level=0) - common.show("Deleting all dependencies...", log=False) + common.show("Deleting all dependencies...", color='message', log=False) common.show() config.uninstall_deps() @@ -192,7 +200,7 @@ def show(*names, root=None): return False for name in names or [None]: - common.show(config.get_path(name)) + common.show(config.get_path(name), color='path') return True diff --git a/gitman/common.py b/gitman/common.py index f0e7f5f3..13a2942e 100644 --- a/gitman/common.py +++ b/gitman/common.py @@ -106,11 +106,54 @@ def dedent(level=None): _Config.indent_level = level -def show(message="", file=sys.stdout, log=logging.getLogger(__name__)): +def show(message="", color=None, + file=sys.stdout, log=logging.getLogger(__name__)): """Write to standard output or error if enabled.""" if _Config.verbosity == 0: - print(" " * _Config.indent_level + message, file=file) + print(' ' * 2 * _Config.indent_level + style(message, color), file=file) elif _Config.verbosity >= 1: message = message.strip() if message and log: - log.info(message) + if color == 'error': + log.error(message) + else: + log.info(message) + + +BOLD = '\033[1m' +RED = '\033[31m' +GREEN = '\033[32m' +YELLOW = '\033[33m' +BLUE = '\033[34m' +MAGENTA = '\033[35m' +CYAN = '\033[36m' +WHITE = '\033[37m' +RESET = '\033[0m' + +COLORS = dict( + revision=BOLD + BLUE, + dirty=BOLD + MAGENTA, + path='', + changes=YELLOW, + message=BOLD + WHITE, + error=BOLD + RED, +) + + +def style(msg, name, tty=None): + color_support = sys.stdout.isatty() if tty is None else tty + if not color_support: + return msg + + if name == 'shell': + return msg.replace("$ ", BOLD + GREEN + "$ " + RESET) + + color = COLORS.get(name) + if color: + return color + msg + RESET + + if msg: + assert color is not None + return msg + + return "" diff --git a/gitman/git.py b/gitman/git.py index e5e83720..1e54463a 100644 --- a/gitman/git.py +++ b/gitman/git.py @@ -66,7 +66,7 @@ def changes(include_untracked=False, display_status=True, _show=False): if status and display_status: for line in git('status', _show=True).splitlines(): - common.show(line) + common.show(line, color='changes') return status diff --git a/gitman/models/source.py b/gitman/models/source.py index 5c5aa854..34aa8ae6 100644 --- a/gitman/models/source.py +++ b/gitman/models/source.py @@ -40,10 +40,10 @@ def __repr__(self): return "".format(self) def __str__(self): - fmt = "'{r}' @ '{v}' in '{d}'" + pattern = "'{r}' @ '{v}' in '{d}'" if self.link: - fmt += " <- '{s}'" - return fmt.format(r=self.repo, v=self.rev, d=self.name, s=self.link) + pattern += " <- '{s}'" + return pattern.format(r=self.repo, v=self.rev, d=self.name, s=self.link) def __eq__(self, other): return self.name == other.name @@ -69,7 +69,7 @@ def update_files(self, force=False, fetch=False, clean=True): log.debug("Confirming there are no uncommitted changes...") if git.changes(include_untracked=clean): common.show() - msg = "Uncommitted changes: {}".format(os.getcwd()) + msg = "Uncommitted changes in {}".format(os.getcwd()) raise UncommittedChanges(msg) # Fetch the desired revision @@ -94,7 +94,7 @@ def create_link(self, root, force=False): shell.rm(target) else: common.show() - msg = "Preexisting link location: {}".format(target) + msg = "Preexisting link location at {}".format(target) raise UncommittedChanges(msg) shell.ln(source, target) @@ -107,16 +107,17 @@ def identify(self, allow_dirty=True, allow_missing=True): path = os.getcwd() url = git.get_url() if git.changes(display_status=not allow_dirty, _show=True): - revision = self.DIRTY if not allow_dirty: common.show() - msg = "Uncommitted changes: {}".format(os.getcwd()) + msg = "Uncommitted changes in {}".format(os.getcwd()) raise UncommittedChanges(msg) + + common.show(self.DIRTY, color='dirty', log=False) + return path, url, self.DIRTY else: revision = git.get_hash(_show=True) - common.show(revision, log=False) - - return path, url, revision + common.show(revision, color='revision', log=False) + return path, url, revision elif allow_missing: diff --git a/gitman/shell.py b/gitman/shell.py index da890ece..09f52c90 100644 --- a/gitman/shell.py +++ b/gitman/shell.py @@ -24,7 +24,7 @@ def call(name, *args, _show=True, _ignore=False): """ program = CMD_PREFIX + ' '.join([name, *args]) if _show: - common.show(program) + common.show(program, color='shell') else: log.debug(program) diff --git a/tests/test_cli.py b/tests/test_cli.py index 4900d01a..fd9e2fd9 100644 --- a/tests/test_cli.py +++ b/tests/test_cli.py @@ -32,13 +32,15 @@ def describe_show(): def it_prints_location_by_default(show, location): cli.main(['show']) - expect(show.mock_calls) == [call(location)] + expect(show.mock_calls) == [call(location, color='path')] @patch('gitman.common.show') def it_can_print_a_depenendcy_path(show, location): cli.main(['show', 'bar']) - expect(show.mock_calls) == [call(os.path.join(location, "bar"))] + expect(show.mock_calls) == [ + call(os.path.join(location, "bar"), color='path'), + ] def it_exits_when_no_config_found(tmpdir): tmpdir.chdir() From 5c71cc25beaae072cbe5506a4bd51c6870119261 Mon Sep 17 00:00:00 2001 From: Jace Browning Date: Thu, 5 Jan 2017 19:00:35 -0500 Subject: [PATCH 17/38] Refactor methods names: "deps" => "dependencies" --- Makefile | 8 ++++---- gitman/commands.py | 22 +++++++++++++--------- gitman/models/config.py | 26 +++++++++++++------------- gitman/tests/test_models_config.py | 24 ++++++++++++------------ 4 files changed, 42 insertions(+), 38 deletions(-) diff --git a/Makefile b/Makefile index 8361238b..0dffe477 100644 --- a/Makefile +++ b/Makefile @@ -150,7 +150,7 @@ COVERAGE_SPACE := $(BIN)/coverage.space RANDOM_SEED ?= $(shell date +%s) -PYTEST_CORE_OPTS := -r xXw -vv +PYTEST_CORE_OPTS := -ra -vv PYTEST_COV_OPTS := --cov=$(PACKAGE) --no-cov-on-fail --cov-report=term-missing --cov-report=html PYTEST_RANDOM_OPTS := --random --random-seed=$(RANDOM_SEED) @@ -172,14 +172,14 @@ test-unit: install ## Run the unit tests .PHONY: test-int test-int: install ## Run the integration tests - @ if test -e $(FAILURES); then $(PYTEST) $(PYTEST_OPTS_FAILFAST) tests; fi + @ if test -e $(FAILURES); then TEST_INTEGRATION=true $(PYTEST) $(PYTEST_OPTS_FAILFAST) tests; fi $(PYTEST) $(PYTEST_OPTS) tests --junitxml=$(REPORTS)/integration.xml $(COVERAGE_SPACE) $(REPOSITORY) integration .PHONY: test-all test-all: install ## Run all the tests - @ if test -e $(FAILURES); then $(PYTEST) $(PYTEST_OPTS_FAILFAST) $(PACKAGES); fi - $(PYTEST) $(PYTEST_OPTS) $(PACKAGES) --junitxml=$(REPORTS)/overall.xml + @ if test -e $(FAILURES); then TEST_INTEGRATION=true $(PYTEST) $(PYTEST_OPTS_FAILFAST) $(PACKAGES); fi + TEST_INTEGRATION=true $(PYTEST) $(PYTEST_OPTS) $(PACKAGES) --junitxml=$(REPORTS)/overall.xml $(COVERAGE_SPACE) $(REPOSITORY) overall .PHONY: read-coverage diff --git a/gitman/commands.py b/gitman/commands.py index 6ef70a34..c7df12a1 100644 --- a/gitman/commands.py +++ b/gitman/commands.py @@ -48,8 +48,10 @@ def install(*names, root=None, depth=None, common.show() common.show("Installing dependencies...", color='message', log=False) common.show() - count = config.install_deps(*names, update=False, depth=depth, - force=force, fetch=fetch, clean=clean) + count = config.install_dependencies( + *names, update=False, depth=depth, + force=force, fetch=fetch, clean=clean, + ) return _display_result("install", "Installed", count) @@ -83,15 +85,16 @@ def update(*names, root=None, depth=None, common.show() common.show("Updating dependencies...", color='message', log=False) common.show() - count = config.install_deps( + count = config.install_dependencies( *names, update=True, depth=depth, - recurse=recurse, force=force, fetch=True, clean=clean) + recurse=recurse, force=force, fetch=True, clean=clean, + ) common.dedent(level=0) if count and lock is not False: common.show("Recording installed versions...", color='message', log=False) common.show() - config.lock_deps(*names, obey_existing=lock is None) + config.lock_dependencies(*names, obey_existing=lock is None) return _display_result("update", "Updated", count) @@ -120,7 +123,8 @@ def display(*, root=None, depth=None, allow_dirty=True): common.show() config.log(datetime.datetime.now().strftime("%F %T")) count = 0 - for identity in config.get_deps(depth=depth, allow_dirty=allow_dirty): + for identity in config.get_dependencies(depth=depth, + allow_dirty=allow_dirty): count += 1 config.log("{}: {} @ {}", *identity) config.log() @@ -148,7 +152,7 @@ def lock(*names, root=None): common.show() common.show("Locking dependencies...", color='message', log=False) common.show() - count = config.lock_deps(*names, obey_existing=False) + count = config.lock_dependencies(*names, obey_existing=False) common.dedent(level=0) return _display_result("lock", "Locked", count) @@ -175,11 +179,11 @@ def delete(*, root=None, force=False): common.show("Checking for uncommitted changes...", color='message', log=False) common.show() - count = len(list(config.get_deps(allow_dirty=force))) + count = len(list(config.get_dependencies(allow_dirty=force))) common.dedent(level=0) common.show("Deleting all dependencies...", color='message', log=False) common.show() - config.uninstall_deps() + config.uninstall_dependencies() return _display_result("delete", "Deleted", count, allow_zero=True) diff --git a/gitman/models/config.py b/gitman/models/config.py index 05a4cce8..91624f96 100644 --- a/gitman/models/config.py +++ b/gitman/models/config.py @@ -43,7 +43,7 @@ def log_path(self): @property def location_path(self): - """Get the full path to the sources location.""" + """Get the full path to the dependency storage location.""" return os.path.join(self.root, self.location) def get_path(self, name=None): @@ -58,10 +58,10 @@ def get_path(self, name=None): else: return base - def install_deps(self, *names, depth=None, - update=True, recurse=False, - force=False, fetch=False, clean=True): - """Get all sources.""" + def install_dependencies(self, *names, depth=None, + update=True, recurse=False, + force=False, fetch=False, clean=True): + """Download or update the specified dependencies.""" if depth == 0: log.info("Skipped directory: %s", self.location_path) return 0 @@ -92,7 +92,7 @@ def install_deps(self, *names, depth=None, config = load_config() if config: common.indent() - count += config.install_deps( + count += config.install_dependencies( depth=None if depth is None else max(0, depth - 1), update=update and recurse, recurse=recurse, @@ -111,7 +111,7 @@ def install_deps(self, *names, depth=None, return count - def lock_deps(self, *names, obey_existing=True): + def lock_dependencies(self, *names, obey_existing=True): """Lock down the immediate dependency versions.""" shell.cd(self.location_path) common.show() @@ -143,12 +143,12 @@ def lock_deps(self, *names, obey_existing=True): return count - def uninstall_deps(self): - """Remove the sources location.""" + def uninstall_dependencies(self): + """Delete the dependency storage location.""" shell.rm(self.location_path) common.show() - def get_deps(self, depth=None, allow_dirty=True): + def get_dependencies(self, depth=None, allow_dirty=True): """Yield the path, repository URL, and hash of each dependency.""" if not os.path.exists(self.location_path): return @@ -169,7 +169,7 @@ def get_deps(self, depth=None, allow_dirty=True): config = load_config() if config: common.indent() - yield from config.get_deps( + yield from config.get_dependencies( depth=None if depth is None else max(0, depth - 1), allow_dirty=allow_dirty, ) @@ -185,7 +185,7 @@ def log(self, message="", *args): outfile.write(message.format(*args) + '\n') def _get_sources(self, *, use_locked=None): - """Merge source lists using requested section as the base.""" + """Merge source lists using the requested section as the base.""" if use_locked is True: if self.sources_locked: return self.sources_locked @@ -198,7 +198,7 @@ def _get_sources(self, *, use_locked=None): sources = self.sources else: if self.sources_locked: - log.info("Defalting to locked sources...") + log.info("Defaulting to locked sources...") sources = self.sources_locked else: log.info("No locked sources, using latest...") diff --git a/gitman/tests/test_models_config.py b/gitman/tests/test_models_config.py index 2c102bb0..b27feabd 100644 --- a/gitman/tests/test_models_config.py +++ b/gitman/tests/test_models_config.py @@ -44,45 +44,45 @@ def test_install_and_list(self): """Verify the correct dependencies are installed.""" config = Config(FILES) - count = config.install_deps() + count = config.install_dependencies() assert 7 == count - deps = list(config.get_deps()) + deps = list(config.get_dependencies()) assert 7 == len(deps) - assert 'eb37743011a398b208dd9f9ef79a408c0fc10d48' == deps[0][2] - assert 'ddbe17ef173538d1fda29bd99a14bab3c5d86e78' == deps[1][2] + assert '1de84ca1d315f81b035cd7b0ecf87ca2025cdacd' == deps[0][2] + assert '050290bca3f14e13fd616604202b579853e7bfb0' == deps[1][2] assert 'fb693447579235391a45ca170959b5583c5042d8' == deps[2][2] # master branch always changes --------------------- deps[3][2] # master branch always changes --------------------- deps[4][2] assert '7bd138fe7359561a8c2ff9d195dff238794ccc04' == deps[5][2] assert '2da24fca34af3748e3cab61db81a2ae8b35aec94' == deps[6][2] - assert 5 == len(list(config.get_deps(depth=2))) + assert 5 == len(list(config.get_dependencies(depth=2))) - assert 3 == len(list(config.get_deps(depth=1))) + assert 3 == len(list(config.get_dependencies(depth=1))) - assert 0 == len(list(config.get_deps(depth=0))) + assert 0 == len(list(config.get_dependencies(depth=0))) @pytest.mark.integration def test_install_with_dirs(self): """Verify the dependency list can be filtered.""" config = Config(FILES) - count = config.install_deps('gitman_2', 'gitman_3') + count = config.install_dependencies('gitman_2', 'gitman_3') assert 2 == count def test_install_with_dirs_unknown(self): """Verify zero dependencies are installed with an unknown dependency.""" config = Config(FILES) - count = config.install_deps('foobar') + count = config.install_dependencies('foobar') assert 0 == count def test_install_with_depth_0(self): """Verify an install depth of 0 installs nothing.""" config = Config(FILES) - count = config.install_deps(depth=0) + count = config.install_dependencies(depth=0) assert 0 == count @pytest.mark.integration @@ -90,7 +90,7 @@ def test_install_with_depth_1(self): """Verify an install depth of 1 installs the direct dependencies.""" config = Config(FILES) - count = config.install_deps(depth=1) + count = config.install_dependencies(depth=1) assert 3 == count @pytest.mark.integration @@ -98,7 +98,7 @@ def test_install_with_depth_2(self): """Verify an install depth of 2 installs 1 level of nesting.""" config = Config(FILES) - count = config.install_deps(depth=2) + count = config.install_dependencies(depth=2) assert 5 == count From d63ee91ab2283af7df467cd0c562ae985be043a6 Mon Sep 17 00:00:00 2001 From: nomorgan Date: Fri, 6 Jan 2017 02:06:21 +0100 Subject: [PATCH 18/38] minimal windows support : add log warning and try to doing only minimal change --- gitman/shell.py | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/gitman/shell.py b/gitman/shell.py index a964c454..e5b886b3 100644 --- a/gitman/shell.py +++ b/gitman/shell.py @@ -59,12 +59,10 @@ def call(name, *args, _show=True, _ignore=False): def mkdir(path): os.mkdir(path) - # call('mkdir', '-p', path) def cd(path, _show=True): - os.chdir(path) - #call('cd', path, _show=_show) + call('cd', path, _show=_show) def ln(source, target): @@ -73,11 +71,11 @@ def ln(source, target): if not os.path.isdir(dirpath): mkdir(dirpath) call('ln', '-s', source, target) - + else: + log.debug("symlinks are not supported on windows system") def rm(path): if not os.path.isdir(dirpath): os.remove(path) else: shutil.rmtree(path) - #call('rm', '-rf', path) From b9aef574d8e0336a9efdf5f2157ecc02d4e0a0a3 Mon Sep 17 00:00:00 2001 From: nomorgan Date: Fri, 6 Jan 2017 03:18:44 +0100 Subject: [PATCH 19/38] #132 minimal windows support : fix path used in pytest + disable some pytest about symlink + fix shell.mkdir + change path management inside gitman.git and gitman.models.config --- gitman/git.py | 3 ++- gitman/models/config.py | 2 +- gitman/shell.py | 9 ++++++--- gitman/tests/test_git.py | 8 ++++---- gitman/tests/test_models_config.py | 4 ++-- gitman/tests/test_shell.py | 10 +++++++--- 6 files changed, 22 insertions(+), 14 deletions(-) diff --git a/gitman/git.py b/gitman/git.py index e5e83720..09c3fb0e 100644 --- a/gitman/git.py +++ b/gitman/git.py @@ -18,6 +18,7 @@ def git(*args, **kwargs): def clone(repo, path, *, cache=None): """Clone a new Git repository.""" cache = cache or os.path.expanduser("~/.gitcache") + cache = os.path.normpath(cache) name = repo.split('/')[-1] if name.endswith(".git"): @@ -27,7 +28,7 @@ def clone(repo, path, *, cache=None): if not os.path.isdir(reference): git('clone', '--mirror', repo, reference) - git('clone', '--reference', reference, repo, path) + git('clone', '--reference', reference, repo, os.path.normpath(path)) def fetch(repo, rev=None): diff --git a/gitman/models/config.py b/gitman/models/config.py index 05a4cce8..9ce2bc19 100644 --- a/gitman/models/config.py +++ b/gitman/models/config.py @@ -33,7 +33,7 @@ def __init__(self, root, filename="gitman.yml", location="gitman_sources"): @property def config_path(self): """Get the full path to the configuration file.""" - return os.path.join(self.root, self.filename) + return os.path.normpath(os.path.join(self.root, self.filename)) path = config_path @property diff --git a/gitman/shell.py b/gitman/shell.py index e5b886b3..f1d764de 100644 --- a/gitman/shell.py +++ b/gitman/shell.py @@ -35,6 +35,7 @@ def call(name, *args, _show=True, _ignore=False): command = subprocess.run( [name, *args], universal_newlines=True, stdout=subprocess.PIPE, stderr=subprocess.STDOUT, + shell=True ) for line in command.stdout.splitlines(): @@ -58,7 +59,8 @@ def call(name, *args, _show=True, _ignore=False): def mkdir(path): - os.mkdir(path) + if not os.path.exists(path): + os.makedirs(path) def cd(path, _show=True): @@ -66,7 +68,7 @@ def cd(path, _show=True): def ln(source, target): - if not os.name=='nt': + if not os.name == 'nt': dirpath = os.path.dirname(target) if not os.path.isdir(dirpath): mkdir(dirpath) @@ -74,8 +76,9 @@ def ln(source, target): else: log.debug("symlinks are not supported on windows system") + def rm(path): - if not os.path.isdir(dirpath): + if not os.path.isdir(path): os.remove(path) else: shutil.rmtree(path) diff --git a/gitman/tests/test_git.py b/gitman/tests/test_git.py index b30c758f..24e1726f 100644 --- a/gitman/tests/test_git.py +++ b/gitman/tests/test_git.py @@ -1,7 +1,7 @@ # pylint: disable=no-self-use from unittest.mock import patch, Mock - +import os from gitman import git from gitman.exceptions import ShellError @@ -17,15 +17,15 @@ def test_clone(self, mock_call): """Verify the commands to set up a new reference repository.""" git.clone('mock.git', 'mock/path', cache='cache') assert_calls(mock_call, [ - "git clone --mirror mock.git cache/mock.reference", - "git clone --reference cache/mock.reference mock.git mock/path"]) + "git clone --mirror mock.git " + os.path.normpath("cache/mock.reference"), + "git clone --reference " + os.path.normpath("cache/mock.reference") + " mock.git " + os.path.normpath("mock/path")]) @patch('os.path.isdir', Mock(return_value=True)) def test_clone_from_reference(self, mock_call): """Verify the commands to clone a Git repository from a reference.""" git.clone('mock.git', 'mock/path', cache='cache') assert_calls(mock_call, [ - "git clone --reference cache/mock.reference mock.git mock/path"]) + "git clone --reference " + os.path.normpath("cache/mock.reference") + " mock.git " + os.path.normpath("mock/path")]) def test_fetch(self, mock_call): """Verify the commands to fetch from a Git repository.""" diff --git a/gitman/tests/test_models_config.py b/gitman/tests/test_models_config.py index 2c102bb0..13d8cd42 100644 --- a/gitman/tests/test_models_config.py +++ b/gitman/tests/test_models_config.py @@ -2,7 +2,7 @@ import pytest from expecter import expect - +import os from gitman.models import Config, load_config from .conftest import FILES @@ -37,7 +37,7 @@ def test_path(self): """Verify a configuration's path is correct.""" config = Config('mock/root') - assert "mock/root/gitman.yml" == config.path + assert os.path.normpath("mock/root/gitman.yml") == config.path @pytest.mark.integration def test_install_and_list(self): diff --git a/gitman/tests/test_shell.py b/gitman/tests/test_shell.py index dd8499a6..d8239cbe 100644 --- a/gitman/tests/test_shell.py +++ b/gitman/tests/test_shell.py @@ -3,7 +3,7 @@ from unittest.mock import patch, Mock import pytest - +import os from gitman import shell from gitman.exceptions import ShellError @@ -31,8 +31,8 @@ def test_other_error_ignored(self): def test_other_capture(self): """Verify a program's output can be captured.""" - stdout = shell.call('echo', 'Hello, world!\n') - assert "Hello, world!" == stdout + stdout = shell.call('echo', "Hello, world!") + assert "\"Hello, world!\"" == stdout @patch('gitman.shell.call') @@ -40,6 +40,7 @@ class TestPrograms: """Tests for calls to shell programs.""" + @pytest.mark.skip(reason="gitman.shell.mkdir do not use call function for now") def test_mkdir(self, mock_call): """Verify the commands to create directories.""" shell.mkdir('mock/name/path') @@ -51,18 +52,21 @@ def test_cd(self, mock_call): assert_calls(mock_call, ["cd mock/name/path"]) @patch('os.path.isdir', Mock(return_value=True)) + @pytest.mark.skipif(os.name == 'nt',reason="symlink not supported on windows") def test_ln(self, mock_call): """Verify the commands to create symbolic links.""" shell.ln('mock/target', 'mock/source') assert_calls(mock_call, ["ln -s mock/target mock/source"]) @patch('os.path.isdir', Mock(return_value=False)) + @pytest.mark.skipif(os.name == 'nt',reason="symlink not supported on windows") def test_ln_missing_parent(self, mock_call): """Verify the commands to create symbolic links (missing parent).""" shell.ln('mock/target', 'mock/source') assert_calls(mock_call, ["mkdir -p mock", "ln -s mock/target mock/source"]) + @pytest.mark.skip(reason="gitman.shell.rm do not use call function for now") def test_rm(self, mock_call): """Verify the commands to delete files/folders.""" shell.rm('mock/name/path') From c5861f73d5b891906865d499260d861045aa3e78 Mon Sep 17 00:00:00 2001 From: nomorgan Date: Fri, 6 Jan 2017 03:24:58 +0100 Subject: [PATCH 20/38] #132 - minimal windows support : gitman.shell.call function revert back to simple command launch without shell --- gitman/shell.py | 1 - 1 file changed, 1 deletion(-) diff --git a/gitman/shell.py b/gitman/shell.py index f1d764de..d46c1047 100644 --- a/gitman/shell.py +++ b/gitman/shell.py @@ -35,7 +35,6 @@ def call(name, *args, _show=True, _ignore=False): command = subprocess.run( [name, *args], universal_newlines=True, stdout=subprocess.PIPE, stderr=subprocess.STDOUT, - shell=True ) for line in command.stdout.splitlines(): From 84be0f29dd8485e5475d2ee6c1988f2ee9bff36b Mon Sep 17 00:00:00 2001 From: nomorgan Date: Fri, 6 Jan 2017 03:48:09 +0100 Subject: [PATCH 21/38] #132 - minimal windows support : gitman.shell.call really need shell=True for some windows BATCH command, but not for regular program --- gitman/shell.py | 18 +++++++++++------- gitman/tests/test_shell.py | 12 ++++++++---- 2 files changed, 19 insertions(+), 11 deletions(-) diff --git a/gitman/shell.py b/gitman/shell.py index d46c1047..158a5162 100644 --- a/gitman/shell.py +++ b/gitman/shell.py @@ -14,14 +14,16 @@ log = logging.getLogger(__name__) -def call(name, *args, _show=True, _ignore=False): - """Call a shell program with arguments. +def call(name, *args, _show=True, _ignore=False, _shell=False): + """Call a program with arguments. :param name: name of program to call :param args: list of command-line arguments :param _show: display the call on stdout :param _ignore: ignore non-zero return codes - + :param _shell: force executing the program into a real shell + a windows shell command (i.e : dir or echo) needs a real shell + but not a regular program (i.e : calc or git) """ program = CMD_PREFIX + ' '.join([name, *args]) if _show: @@ -35,6 +37,7 @@ def call(name, *args, _show=True, _ignore=False): command = subprocess.run( [name, *args], universal_newlines=True, stdout=subprocess.PIPE, stderr=subprocess.STDOUT, + shell=_shell ) for line in command.stdout.splitlines(): @@ -77,7 +80,8 @@ def ln(source, target): def rm(path): - if not os.path.isdir(path): - os.remove(path) - else: - shutil.rmtree(path) + if os.path.exists(path): + if not os.path.isdir(path): + os.remove(path) + else: + shutil.rmtree(path) diff --git a/gitman/tests/test_shell.py b/gitman/tests/test_shell.py index d8239cbe..a8576df9 100644 --- a/gitman/tests/test_shell.py +++ b/gitman/tests/test_shell.py @@ -31,8 +31,12 @@ def test_other_error_ignored(self): def test_other_capture(self): """Verify a program's output can be captured.""" - stdout = shell.call('echo', "Hello, world!") - assert "\"Hello, world!\"" == stdout + if os.name == 'nt': + stdout = shell.call('echo', 'Hello, world!', _shell=True) + assert '"Hello, world!"' == stdout + else: + stdout = shell.call('echo', 'Hello, world!\n') + assert "Hello, world!" == stdout @patch('gitman.shell.call') @@ -52,14 +56,14 @@ def test_cd(self, mock_call): assert_calls(mock_call, ["cd mock/name/path"]) @patch('os.path.isdir', Mock(return_value=True)) - @pytest.mark.skipif(os.name == 'nt',reason="symlink not supported on windows") + @pytest.mark.skipif(os.name == 'nt', reason="symlink not supported on windows") def test_ln(self, mock_call): """Verify the commands to create symbolic links.""" shell.ln('mock/target', 'mock/source') assert_calls(mock_call, ["ln -s mock/target mock/source"]) @patch('os.path.isdir', Mock(return_value=False)) - @pytest.mark.skipif(os.name == 'nt',reason="symlink not supported on windows") + @pytest.mark.skipif(os.name == 'nt', reason="symlink not supported on windows") def test_ln_missing_parent(self, mock_call): """Verify the commands to create symbolic links (missing parent).""" shell.ln('mock/target', 'mock/source') From 32543b1e352620a1d69187471848bc21ad5b727d Mon Sep 17 00:00:00 2001 From: nomorgan Date: Fri, 6 Jan 2017 03:53:27 +0100 Subject: [PATCH 22/38] #132 - minimal windows support : fix useless whitespace + in test remove assert on print mkdir command --- gitman/shell.py | 2 +- gitman/tests/test_shell.py | 3 +-- 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/gitman/shell.py b/gitman/shell.py index 158a5162..2c86d090 100644 --- a/gitman/shell.py +++ b/gitman/shell.py @@ -21,7 +21,7 @@ def call(name, *args, _show=True, _ignore=False, _shell=False): :param args: list of command-line arguments :param _show: display the call on stdout :param _ignore: ignore non-zero return codes - :param _shell: force executing the program into a real shell + :param _shell: force executing the program into a real shell a windows shell command (i.e : dir or echo) needs a real shell but not a regular program (i.e : calc or git) """ diff --git a/gitman/tests/test_shell.py b/gitman/tests/test_shell.py index a8576df9..430c1c0d 100644 --- a/gitman/tests/test_shell.py +++ b/gitman/tests/test_shell.py @@ -67,8 +67,7 @@ def test_ln(self, mock_call): def test_ln_missing_parent(self, mock_call): """Verify the commands to create symbolic links (missing parent).""" shell.ln('mock/target', 'mock/source') - assert_calls(mock_call, ["mkdir -p mock", - "ln -s mock/target mock/source"]) + assert_calls(mock_call, ["ln -s mock/target mock/source"]) @pytest.mark.skip(reason="gitman.shell.rm do not use call function for now") def test_rm(self, mock_call): From ed9a3c2603749d03e600873e72dce4dcc441e58f Mon Sep 17 00:00:00 2001 From: nomorgan Date: Fri, 6 Jan 2017 04:03:34 +0100 Subject: [PATCH 23/38] #132 - minimal windows support : fix pylint guidelines... --- gitman/shell.py | 4 ++-- gitman/tests/test_git.py | 12 +++++++++--- gitman/tests/test_models_config.py | 3 ++- gitman/tests/test_shell.py | 11 ++++++----- 4 files changed, 19 insertions(+), 11 deletions(-) diff --git a/gitman/shell.py b/gitman/shell.py index 2c86d090..58908a0d 100644 --- a/gitman/shell.py +++ b/gitman/shell.py @@ -22,8 +22,8 @@ def call(name, *args, _show=True, _ignore=False, _shell=False): :param _show: display the call on stdout :param _ignore: ignore non-zero return codes :param _shell: force executing the program into a real shell - a windows shell command (i.e : dir or echo) needs a real shell - but not a regular program (i.e : calc or git) + a windows shell command (i.e : dir, echo) needs a real shell + but not a regular program (i.e : calc, git) """ program = CMD_PREFIX + ' '.join([name, *args]) if _show: diff --git a/gitman/tests/test_git.py b/gitman/tests/test_git.py index 24e1726f..37de3395 100644 --- a/gitman/tests/test_git.py +++ b/gitman/tests/test_git.py @@ -17,15 +17,21 @@ def test_clone(self, mock_call): """Verify the commands to set up a new reference repository.""" git.clone('mock.git', 'mock/path', cache='cache') assert_calls(mock_call, [ - "git clone --mirror mock.git " + os.path.normpath("cache/mock.reference"), - "git clone --reference " + os.path.normpath("cache/mock.reference") + " mock.git " + os.path.normpath("mock/path")]) + "git clone --mirror mock.git " + + os.path.normpath("cache/mock.reference"), + "git clone --reference " + + os.path.normpath("cache/mock.reference") + + " mock.git " + os.path.normpath("mock/path")]) @patch('os.path.isdir', Mock(return_value=True)) def test_clone_from_reference(self, mock_call): """Verify the commands to clone a Git repository from a reference.""" git.clone('mock.git', 'mock/path', cache='cache') assert_calls(mock_call, [ - "git clone --reference " + os.path.normpath("cache/mock.reference") + " mock.git " + os.path.normpath("mock/path")]) + "git clone --reference " + + os.path.normpath("cache/mock.reference") + + " mock.git " + + os.path.normpath("mock/path")]) def test_fetch(self, mock_call): """Verify the commands to fetch from a Git repository.""" diff --git a/gitman/tests/test_models_config.py b/gitman/tests/test_models_config.py index 13d8cd42..06fa99d1 100644 --- a/gitman/tests/test_models_config.py +++ b/gitman/tests/test_models_config.py @@ -1,8 +1,9 @@ # pylint: disable=no-self-use,redefined-outer-name,unused-variable,expression-not-assigned,misplaced-comparison-constant +import os import pytest from expecter import expect -import os + from gitman.models import Config, load_config from .conftest import FILES diff --git a/gitman/tests/test_shell.py b/gitman/tests/test_shell.py index 430c1c0d..531d39d8 100644 --- a/gitman/tests/test_shell.py +++ b/gitman/tests/test_shell.py @@ -1,9 +1,10 @@ # pylint: disable=no-self-use,misplaced-comparison-constant +import os + from unittest.mock import patch, Mock import pytest -import os from gitman import shell from gitman.exceptions import ShellError @@ -44,7 +45,7 @@ class TestPrograms: """Tests for calls to shell programs.""" - @pytest.mark.skip(reason="gitman.shell.mkdir do not use call function for now") + @pytest.mark.skip(reason="gitman.shell.mkdir do not use call function") def test_mkdir(self, mock_call): """Verify the commands to create directories.""" shell.mkdir('mock/name/path') @@ -56,20 +57,20 @@ def test_cd(self, mock_call): assert_calls(mock_call, ["cd mock/name/path"]) @patch('os.path.isdir', Mock(return_value=True)) - @pytest.mark.skipif(os.name == 'nt', reason="symlink not supported on windows") + @pytest.mark.skipif(os.name == 'nt', reason="no symlink on windows") def test_ln(self, mock_call): """Verify the commands to create symbolic links.""" shell.ln('mock/target', 'mock/source') assert_calls(mock_call, ["ln -s mock/target mock/source"]) @patch('os.path.isdir', Mock(return_value=False)) - @pytest.mark.skipif(os.name == 'nt', reason="symlink not supported on windows") + @pytest.mark.skipif(os.name == 'nt', reason="no symlink on windows") def test_ln_missing_parent(self, mock_call): """Verify the commands to create symbolic links (missing parent).""" shell.ln('mock/target', 'mock/source') assert_calls(mock_call, ["ln -s mock/target mock/source"]) - @pytest.mark.skip(reason="gitman.shell.rm do not use call function for now") + @pytest.mark.skip(reason="gitman.shell.rm do not use call function") def test_rm(self, mock_call): """Verify the commands to delete files/folders.""" shell.rm('mock/name/path') From 3538f74ada46ccca14cdf5892f952b39dc9e488e Mon Sep 17 00:00:00 2001 From: nomorgan Date: Fri, 6 Jan 2017 04:09:58 +0100 Subject: [PATCH 24/38] #132 - minimal windows support : fix pep8 guidelines... --- gitman/tests/test_git.py | 20 +++++++++++--------- 1 file changed, 11 insertions(+), 9 deletions(-) diff --git a/gitman/tests/test_git.py b/gitman/tests/test_git.py index 37de3395..d665b2cb 100644 --- a/gitman/tests/test_git.py +++ b/gitman/tests/test_git.py @@ -17,21 +17,23 @@ def test_clone(self, mock_call): """Verify the commands to set up a new reference repository.""" git.clone('mock.git', 'mock/path', cache='cache') assert_calls(mock_call, [ - "git clone --mirror mock.git " - + os.path.normpath("cache/mock.reference"), - "git clone --reference " - + os.path.normpath("cache/mock.reference") - + " mock.git " + os.path.normpath("mock/path")]) + "git clone --mirror mock.git " + + os.path.normpath("cache/mock.reference"), + "git clone --reference " + + os.path.normpath("cache/mock.reference") + " mock.git " + os.path.normpath("mock/path") + ]) @patch('os.path.isdir', Mock(return_value=True)) def test_clone_from_reference(self, mock_call): """Verify the commands to clone a Git repository from a reference.""" git.clone('mock.git', 'mock/path', cache='cache') assert_calls(mock_call, [ - "git clone --reference " - + os.path.normpath("cache/mock.reference") - + " mock.git " - + os.path.normpath("mock/path")]) + "git clone --reference " + + os.path.normpath("cache/mock.reference") + + " mock.git " + + os.path.normpath("mock/path") + ]) def test_fetch(self, mock_call): """Verify the commands to fetch from a Git repository.""" From ee3dbd253b8072526fcc48252162de6fd7890676 Mon Sep 17 00:00:00 2001 From: nomorgan Date: Fri, 6 Jan 2017 04:13:11 +0100 Subject: [PATCH 25/38] #132 - minimal windows support : fix pylint guidelines... --- gitman/tests/test_git.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/gitman/tests/test_git.py b/gitman/tests/test_git.py index d665b2cb..854df460 100644 --- a/gitman/tests/test_git.py +++ b/gitman/tests/test_git.py @@ -21,7 +21,8 @@ def test_clone(self, mock_call): os.path.normpath("cache/mock.reference"), "git clone --reference " + os.path.normpath("cache/mock.reference") - " mock.git " + os.path.normpath("mock/path") + " mock.git " + + os.path.normpath("mock/path") ]) @patch('os.path.isdir', Mock(return_value=True)) From 4783a6e531b4fd16947d1cf291f8edccbfe8e461 Mon Sep 17 00:00:00 2001 From: nomorgan Date: Fri, 6 Jan 2017 04:15:51 +0100 Subject: [PATCH 26/38] #132 - minimal windows support : fix pylint guidelines... --- gitman/tests/test_git.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gitman/tests/test_git.py b/gitman/tests/test_git.py index 854df460..6136e263 100644 --- a/gitman/tests/test_git.py +++ b/gitman/tests/test_git.py @@ -20,7 +20,7 @@ def test_clone(self, mock_call): "git clone --mirror mock.git " + os.path.normpath("cache/mock.reference"), "git clone --reference " + - os.path.normpath("cache/mock.reference") + os.path.normpath("cache/mock.reference") + " mock.git " + os.path.normpath("mock/path") ]) From 15af78635fff94b16260bc1157ba70828dd5f3ee Mon Sep 17 00:00:00 2001 From: Jace Browning Date: Fri, 6 Jan 2017 11:56:18 -0500 Subject: [PATCH 27/38] Run each command as part of CI --- Makefile | 14 ++++++++++---- 1 file changed, 10 insertions(+), 4 deletions(-) diff --git a/Makefile b/Makefile index 0dffe477..d7a064c9 100644 --- a/Makefile +++ b/Makefile @@ -65,15 +65,21 @@ HONCHO := $(ACTIVATE) && $(BIN)/honcho all: doc .PHONY: ci -ci: check test ## Run all tasks that determine CI status +ci: check test demo ## Run all tasks that determine CI status .PHONY: watch watch: install .clean-test ## Continuously run all CI tasks when files chanage $(SNIFFER) -.PHONY: run ## Start the program -run: install - $(PYTHON) $(PACKAGE)/__main__.py +.PHONY: demo +demo: install + $(BIN)/gitman install + $(BIN)/gitman update + $(BIN)/gitman list + $(BIN)/gitman lock + $(BIN)/gitman uninstall + $(BIN)/gitman show + $(BIN)/gitman edit # SYSTEM DEPENDENCIES ########################################################## From 72c58292439102bf81c336d92b6fa7d640246a45 Mon Sep 17 00:00:00 2001 From: Jace Browning Date: Fri, 6 Jan 2017 12:16:52 -0500 Subject: [PATCH 28/38] Add Appveyor config --- .appveyor.yml | 19 +++++++++++++++++++ .travis.yml | 1 + 2 files changed, 20 insertions(+) create mode 100644 .appveyor.yml diff --git a/.appveyor.yml b/.appveyor.yml new file mode 100644 index 00000000..a99f3cab --- /dev/null +++ b/.appveyor.yml @@ -0,0 +1,19 @@ +cache: + - env + +install: + # Export build paths + - copy C:\MinGW\bin\mingw32-make.exe C:\MinGW\bin\make.exe + - set PATH=%PATH%;C:\MinGW\bin + - make --version + # Check system dependencies + - make doctor + # Install project dependencies + - make install + +build: off + +test_script: + - make test + - make check + - make demo diff --git a/.travis.yml b/.travis.yml index 66383a79..b658fc5e 100644 --- a/.travis.yml +++ b/.travis.yml @@ -20,6 +20,7 @@ install: script: - make check - make test + - make demo after_success: - pip install coveralls scrutinizer-ocular From 775ec6d7137bbb638e963f123de71f93703ca2d9 Mon Sep 17 00:00:00 2001 From: Jace Browning Date: Fri, 6 Jan 2017 12:20:22 -0500 Subject: [PATCH 29/38] Allow edit command to fail --- Makefile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Makefile b/Makefile index d7a064c9..3a589f14 100644 --- a/Makefile +++ b/Makefile @@ -79,7 +79,7 @@ demo: install $(BIN)/gitman lock $(BIN)/gitman uninstall $(BIN)/gitman show - $(BIN)/gitman edit + - $(BIN)/gitman edit # SYSTEM DEPENDENCIES ########################################################## From ff101dd3b320bd046bfe9772a0fca699e0cda0f3 Mon Sep 17 00:00:00 2001 From: Jace Browning Date: Fri, 6 Jan 2017 12:23:18 -0500 Subject: [PATCH 30/38] Handle permission errors --- tests/test_api.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/test_api.py b/tests/test_api.py index 94e99d12..a6cc2c8b 100644 --- a/tests/test_api.py +++ b/tests/test_api.py @@ -38,7 +38,7 @@ @pytest.fixture def config(root="/tmp/gitman-shared"): - with suppress(FileNotFoundError): + with suppress(FileNotFoundError, PermissionError): shutil.rmtree(root) with suppress(FileExistsError): os.makedirs(root) From 39e2f98908611ee8ff05386b8a6118b14321e085 Mon Sep 17 00:00:00 2001 From: Jace Browning Date: Fri, 6 Jan 2017 12:30:53 -0500 Subject: [PATCH 31/38] Add Appveyor badge --- README.md | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/README.md b/README.md index 718fcb88..ad7bc62a 100644 --- a/README.md +++ b/README.md @@ -1,8 +1,4 @@ -[![Build Status](https://travis-ci.org/jacebrowning/gitman.svg?branch=develop)](https://travis-ci.org/jacebrowning/gitman) -[![Coverage Status](https://coveralls.io/repos/github/jacebrowning/gitman/badge.svg?branch=develop)](https://coveralls.io/github/jacebrowning/gitman?branch=develop) -[![Scrutinizer Code Quality](http://img.shields.io/scrutinizer/g/jacebrowning/gitman.svg)](https://scrutinizer-ci.com/g/jacebrowning/gitman/?branch=master) -[![PyPI Version](http://img.shields.io/pypi/v/GitMan.svg)](https://pypi.python.org/pypi/GitMan) -[![PyPI Downloads](http://img.shields.io/pypi/dm/GitMan.svg)](https://pypi.python.org/pypi/GitMan) +Unix: [![Build Status](https://travis-ci.org/jacebrowning/gitman.svg?branch=develop)](https://travis-ci.org/jacebrowning/gitman) Windows: [![Windows Build Status](https://img.shields.io/appveyor/ci/jacebrowning/gitman/develop.svg)](https://ci.appveyor.com/project/jacebrowning/gitman)
Metrics: [![Coverage Status](https://img.shields.io/coveralls/jacebrowning/gitman/develop.svg)](https://coveralls.io/r/jacebrowning/gitman) [![Scrutinizer Code Quality](https://img.shields.io/scrutinizer/g/jacebrowning/gitman.svg)](https://scrutinizer-ci.com/g/jacebrowning/gitman/?branch=develop)
Usage: [![PyPI Version](https://img.shields.io/pypi/v/GitMan.svg)](https://pypi.python.org/pypi/GitMan) [![PyPI Downloads](https://img.shields.io/pypi/dm/gitman.svg)](https://pypi.python.org/pypi/GitMan) # Overview From 576bebcfd5744a2b733c7127bd18bd86b81375e1 Mon Sep 17 00:00:00 2001 From: Jace Browning Date: Fri, 6 Jan 2017 12:33:28 -0500 Subject: [PATCH 32/38] Document preliminary Windows support --- CHANGELOG.md | 1 + README.md | 1 - setup.py | 3 +-- 3 files changed, 2 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 59af5cf4..4b38b23e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,6 +3,7 @@ ## 1.1 (unreleased) - Added coloring to the command-line output. +- Added preliminary Windows support. ## 1.0.2 (2016/07/28) diff --git a/README.md b/README.md index ad7bc62a..76dd5375 100644 --- a/README.md +++ b/README.md @@ -10,7 +10,6 @@ GitMan is a language-agnostic "dependency manager" using Git. It aims to serve a * Python 3.5+ * Git 2.8+ (with [stored credentials](http://gitman.readthedocs.io/en/latest/setup/git/)) -* Unix shell (or Cygwin/MinGW/etc. on Windows) ## Installation diff --git a/setup.py b/setup.py index 8cb0f4f4..ab4e2cef 100644 --- a/setup.py +++ b/setup.py @@ -68,8 +68,7 @@ def read_descriptions(): 'Intended Audience :: Developers', 'License :: OSI Approved :: MIT License', 'Natural Language :: English', - 'Operating System :: MacOS', - 'Operating System :: POSIX', + 'Operating System :: OS Independent', 'Programming Language :: Python', 'Programming Language :: Python :: 3', 'Programming Language :: Python :: 3.5', From 7649943be110151272a660eb744f49548103ee93 Mon Sep 17 00:00:00 2001 From: Jace Browning Date: Fri, 6 Jan 2017 14:49:15 -0500 Subject: [PATCH 33/38] Log all shell calls as if done manually This is to help train the user on how the shell works. --- gitman/shell.py | 41 +++++++++++++++++-------------- gitman/tests/test_shell.py | 50 +++++++++++++++++++++----------------- 2 files changed, 51 insertions(+), 40 deletions(-) diff --git a/gitman/shell.py b/gitman/shell.py index cd184e4b..5e85b0e0 100644 --- a/gitman/shell.py +++ b/gitman/shell.py @@ -22,17 +22,10 @@ def call(name, *args, _show=True, _ignore=False, _shell=False): :param _show: display the call on stdout :param _ignore: ignore non-zero return codes :param _shell: force executing the program into a real shell - a windows shell command (i.e : dir, echo) needs a real shell - but not a regular program (i.e : calc, git) + a Windows shell command (i.e: dir, echo) needs a real shell + but not a regular program (i.e: calc, git) """ - program = CMD_PREFIX + ' '.join([name, *args]) - if _show: - common.show(program, color='shell') - else: - log.debug(program) - - if name == 'cd': - return os.chdir(args[0]) # 'cd' has no effect in a subprocess + program = show(name, *args, stdout=_show) command = subprocess.run( [name, *args], universal_newlines=True, @@ -47,7 +40,7 @@ def call(name, *args, _show=True, _ignore=False, _shell=False): return command.stdout.strip() elif _ignore: - log.debug("Ignored error from call to '%s'", program) + log.debug("Ignored error from call to '%s'", name) else: message = ( @@ -61,27 +54,39 @@ def call(name, *args, _show=True, _ignore=False, _shell=False): def mkdir(path): + show('mkdir', '-p', path) if not os.path.exists(path): os.makedirs(path) def cd(path, _show=True): - call('cd', path, _show=_show) + show('cd', path, stdout=_show) + os.chdir(path) def ln(source, target): - if not os.name == 'nt': + if os.name == 'nt': + log.warning("Symlinks are not supported on Windows") + else: dirpath = os.path.dirname(target) if not os.path.isdir(dirpath): mkdir(dirpath) call('ln', '-s', source, target) - else: - log.debug("symlinks are not supported on windows system") def rm(path): + show('rm', '-rf', path) if os.path.exists(path): - if not os.path.isdir(path): - os.remove(path) - else: + if os.path.isdir(path): shutil.rmtree(path) + else: + os.remove(path) + + +def show(name, *args, stdout=True): + program = CMD_PREFIX + ' '.join([name, *args]) + if stdout: + common.show(program, color='shell') + else: + log.debug(program) + return program diff --git a/gitman/tests/test_shell.py b/gitman/tests/test_shell.py index 531d39d8..9393631a 100644 --- a/gitman/tests/test_shell.py +++ b/gitman/tests/test_shell.py @@ -12,15 +12,8 @@ class TestCall: - """Tests for interacting with the shell.""" - @patch('os.chdir') - def test_cd(self, mock_chdir): - """Verify directories are changed correctly.""" - shell.call('cd', 'mock/name') - mock_chdir.assert_called_once_with('mock/name') - def test_other_error_uncaught(self): """Verify program errors raise exceptions.""" with pytest.raises(ShellError): @@ -42,36 +35,49 @@ def test_other_capture(self): @patch('gitman.shell.call') class TestPrograms: - """Tests for calls to shell programs.""" - @pytest.mark.skip(reason="gitman.shell.mkdir do not use call function") - def test_mkdir(self, mock_call): + @patch('os.makedirs') + def test_mkdir(self, mock_makedirs, mock_call): """Verify the commands to create directories.""" - shell.mkdir('mock/name/path') - assert_calls(mock_call, ["mkdir -p mock/name/path"]) + shell.mkdir('mock/dirpath') + mock_makedirs.assert_called_once_with('mock/dirpath') + assert_calls(mock_call, []) - def test_cd(self, mock_call): + @patch('os.chdir') + def test_cd(self, mock_chdir, mock_call): """Verify the commands to change directories.""" - shell.cd('mock/name/path') - assert_calls(mock_call, ["cd mock/name/path"]) + shell.cd('mock/dirpath') + mock_chdir.assert_called_once_with('mock/dirpath') + assert_calls(mock_call, []) @patch('os.path.isdir', Mock(return_value=True)) - @pytest.mark.skipif(os.name == 'nt', reason="no symlink on windows") + @pytest.mark.skipif(os.name == 'nt', reason="no symlink on Windows") def test_ln(self, mock_call): """Verify the commands to create symbolic links.""" shell.ln('mock/target', 'mock/source') assert_calls(mock_call, ["ln -s mock/target mock/source"]) @patch('os.path.isdir', Mock(return_value=False)) - @pytest.mark.skipif(os.name == 'nt', reason="no symlink on windows") + @pytest.mark.skipif(os.name == 'nt', reason="no symlink on Windows") def test_ln_missing_parent(self, mock_call): """Verify the commands to create symbolic links (missing parent).""" shell.ln('mock/target', 'mock/source') assert_calls(mock_call, ["ln -s mock/target mock/source"]) - @pytest.mark.skip(reason="gitman.shell.rm do not use call function") - def test_rm(self, mock_call): - """Verify the commands to delete files/folders.""" - shell.rm('mock/name/path') - assert_calls(mock_call, ["rm -rf mock/name/path"]) + @patch('os.remove') + @patch('os.path.exists', Mock(return_value=True)) + def test_rm_file(self, mock_remove, mock_call): + """Verify the commands to delete files.""" + shell.rm('mock/path') + mock_remove.assert_called_once_with('mock/path') + assert_calls(mock_call, []) + + @patch('shutil.rmtree') + @patch('os.path.exists', Mock(return_value=True)) + @patch('os.path.isdir', Mock(return_value=True)) + def test_rm_directory(self, mock_rmtree, mock_call): + """Verify the commands to delete directories.""" + shell.rm('mock/dirpath') + mock_rmtree.assert_called_once_with('mock/dirpath') + assert_calls(mock_call, []) From c94e4beb0cb3b171cb3fae1e78bea55cac64379c Mon Sep 17 00:00:00 2001 From: Jace Browning Date: Fri, 6 Jan 2017 15:35:50 -0500 Subject: [PATCH 34/38] Test with YORM 1.2b4 --- Makefile | 2 +- requirements.txt | 1 - setup.py | 4 +++- 3 files changed, 4 insertions(+), 3 deletions(-) delete mode 100644 requirements.txt diff --git a/Makefile b/Makefile index 3a589f14..9b7527f9 100644 --- a/Makefile +++ b/Makefile @@ -111,7 +111,7 @@ else ifdef LINUX endif @ touch $@ # flag to indicate dependencies are installed -$(DEPS_BASE): setup.py requirements.txt $(PYTHON) +$(DEPS_BASE): setup.py $(PYTHON) $(PYTHON) setup.py develop @ touch $@ # flag to indicate dependencies are installed diff --git a/requirements.txt b/requirements.txt deleted file mode 100644 index 3d0b0123..00000000 --- a/requirements.txt +++ /dev/null @@ -1 +0,0 @@ -YORM ~= 1.0 diff --git a/setup.py b/setup.py index ab4e2cef..f8643a5c 100644 --- a/setup.py +++ b/setup.py @@ -78,5 +78,7 @@ def read_descriptions(): 'Topic :: System :: Software Distribution', ], - install_requires=open("requirements.txt").readlines(), + install_requires=[ + 'YORM==1.2b4', + ], ) From b5b10d2698557be957d6c38552703a02876a8f1d Mon Sep 17 00:00:00 2001 From: Jace Browning Date: Fri, 6 Jan 2017 16:07:29 -0500 Subject: [PATCH 35/38] Never lock a dirty version Fixes #104 --- gitman/models/source.py | 2 +- setup.py | 2 +- tests/test_api.py | 9 ++++++++- 3 files changed, 10 insertions(+), 3 deletions(-) diff --git a/gitman/models/source.py b/gitman/models/source.py index 34aa8ae6..9441561e 100644 --- a/gitman/models/source.py +++ b/gitman/models/source.py @@ -131,6 +131,6 @@ def identify(self, allow_dirty=True, allow_missing=True): def lock(self): """Return a locked version of the current source.""" - _, _, revision = self.identify(allow_missing=False) + _, _, revision = self.identify(allow_dirty=False, allow_missing=False) source = self.__class__(self.repo, self.name, revision, self.link) return source diff --git a/setup.py b/setup.py index f8643a5c..176b14ed 100644 --- a/setup.py +++ b/setup.py @@ -79,6 +79,6 @@ def read_descriptions(): ], install_requires=[ - 'YORM==1.2b4', + 'YORM==1.2b5', ], ) diff --git a/tests/test_api.py b/tests/test_api.py index a6cc2c8b..ed3fc7e7 100644 --- a/tests/test_api.py +++ b/tests/test_api.py @@ -11,7 +11,7 @@ import gitman from gitman.models import Config -from gitman.exceptions import InvalidRepository +from gitman.exceptions import UncommittedChanges, InvalidRepository from .utilities import strip @@ -329,6 +329,13 @@ def it_records_specified_dependencies(config): rev: 9bf18e16b956041f0267c21baad555a23237b52e """) == config.__mapper__.text + def it_should_fail_on_dirty_repositories(config): + expect(gitman.update(depth=1, lock=False)) == True + os.remove("deps/gitman_1/.project") + + with pytest.raises(UncommittedChanges): + gitman.lock() + def it_should_fail_on_invalid_repositories(config): os.system("mkdir deps && touch deps/gitman_1") From eb8fb3497bf056f19c981e12b17bbac1d25d8fe0 Mon Sep 17 00:00:00 2001 From: Jace Browning Date: Fri, 6 Jan 2017 16:20:14 -0500 Subject: [PATCH 36/38] Update list of changes --- CHANGELOG.md | 1 + gitman/__init__.py | 2 +- setup.py | 3 ++- 3 files changed, 4 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 4b38b23e..3c8f4871 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,7 @@ - Added coloring to the command-line output. - Added preliminary Windows support. +- Fixed issue where `` could be saved as a locked revision. ## 1.0.2 (2016/07/28) diff --git a/gitman/__init__.py b/gitman/__init__.py index 145c6c09..08aa7904 100644 --- a/gitman/__init__.py +++ b/gitman/__init__.py @@ -3,7 +3,7 @@ import sys __project__ = 'GitMan' -__version__ = '1.1b1' +__version__ = '1.1b2' CLI = 'gitman' PLUGIN = 'deps' diff --git a/setup.py b/setup.py index 176b14ed..8a65c614 100644 --- a/setup.py +++ b/setup.py @@ -72,6 +72,7 @@ def read_descriptions(): 'Programming Language :: Python', 'Programming Language :: Python :: 3', 'Programming Language :: Python :: 3.5', + 'Programming Language :: Python :: 3.6', 'Topic :: Software Development', 'Topic :: Software Development :: Build Tools', 'Topic :: Software Development :: Version Control', @@ -79,6 +80,6 @@ def read_descriptions(): ], install_requires=[ - 'YORM==1.2b5', + 'YORM~=1.1', ], ) From 744e3b09564ffa945f71983fbf26cc1a1de380a1 Mon Sep 17 00:00:00 2001 From: Jace Browning Date: Fri, 6 Jan 2017 16:31:27 -0500 Subject: [PATCH 37/38] Bump version to 1.1 --- CHANGELOG.md | 2 +- gitman/__init__.py | 6 +++--- setup.py | 2 +- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 3c8f4871..c7f975e2 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,6 @@ # Revision History -## 1.1 (unreleased) +## 1.1 (2017/01/06) - Added coloring to the command-line output. - Added preliminary Windows support. diff --git a/gitman/__init__.py b/gitman/__init__.py index 08aa7904..de78f295 100644 --- a/gitman/__init__.py +++ b/gitman/__init__.py @@ -3,13 +3,13 @@ import sys __project__ = 'GitMan' -__version__ = '1.1b2' +__version__ = '1.1' CLI = 'gitman' PLUGIN = 'deps' -NAME = "Git Dependency Manager" +NAME = 'Git Dependency Manager' VERSION = __project__ + ' v' + __version__ -DESCRIPTION = "A language-agnostic dependency manager using Git." +DESCRIPTION = 'A language-agnostic dependency manager using Git.' PYTHON_VERSION = 3, 5 diff --git a/setup.py b/setup.py index 8a65c614..20f6839f 100644 --- a/setup.py +++ b/setup.py @@ -23,7 +23,7 @@ def read_package_variable(key): module_path = os.path.join(PACKAGE_NAME, '__init__.py') with open(module_path) as module: for line in module: - parts = line.strip().split(' ') + parts = line.strip().split(' ', 2) if parts and parts[0] == key: return parts[-1].strip("'") assert 0, "'{0}' not found in '{1}'".format(key, module_path) From 11641a29115c849acc6400dd2f31933c88ae98f4 Mon Sep 17 00:00:00 2001 From: Jace Browning Date: Fri, 6 Jan 2017 16:53:32 -0500 Subject: [PATCH 38/38] Add demo GIF --- README.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/README.md b/README.md index 76dd5375..9e974726 100644 --- a/README.md +++ b/README.md @@ -4,6 +4,8 @@ Unix: [![Build Status](https://travis-ci.org/jacebrowning/gitman.svg?branch=deve GitMan is a language-agnostic "dependency manager" using Git. It aims to serve as a submodules replacement and provides advanced options for managing versions of nested Git repositories. +![demo](https://raw.githubusercontent.com/jacebrowning/gitman/develop/docs/demo.gif) + # Setup ## Requirements