pax_global_header00006660000000000000000000000064134733160540014520gustar00rootroot0000000000000052 comment=ec9edd1cae7bd99d3a82f4578c9b994604ea64cd gitless-0.8.8/000077500000000000000000000000001347331605400132075ustar00rootroot00000000000000gitless-0.8.8/.appveyor.yml000066400000000000000000000020101347331605400156460ustar00rootroot00000000000000# Based on pygit2's appveyor config environment: matrix: - GENERATOR: 'Visual Studio 10' PYTHON: 'C:\Python27\python.exe' - GENERATOR: 'Visual Studio 10 Win64' PYTHON: 'C:\Python27-x64\python.exe' init: - cmd: '%PYTHON% -m pip install -U nose wheel' build: off before_test: - cmd: git config --global user.name "appveyor-test" - cmd: git config --global user.email "appveyor@test.com" test_script: - ps: | # e2e tests require `gl` binary &$env:PYTHON -m pip install -r requirements.txt . # 'gl' is installed in Python Scripts directory $env:PATH += ";$(Split-Path $env:PYTHON)\Scripts" &$env:PYTHON setup.py nosetests --logging-level=WARN --with-xunit if ($LastExitCode -ne 0) { $host.SetShouldExit($LastExitCode) } # upload results to AppVeyor $wc = New-Object 'System.Net.WebClient' $wc.UploadFile("https://ci.appveyor.com/api/testresults/junit/$($env:APPVEYOR_JOB_ID)", (Resolve-Path .\nosetests.xml)) branches: only: - master - develop gitless-0.8.8/.gitignore000066400000000000000000000005051347331605400151770ustar00rootroot00000000000000# Vi backup files *~ *.py[cod] # C extensions *.so # Packages *.egg *.egg-info dist build eggs parts bin var sdist develop-eggs .installed.cfg lib lib64 # Installer logs pip-log.txt # Unit test / coverage reports .coverage .tox nosetests.xml # Translations *.mo # Mr Developer .mr.developer.cfg .project .pydevproject gitless-0.8.8/.pylint.rc000066400000000000000000000003151347331605400151310ustar00rootroot00000000000000[FORMAT] indent-string=' ' [MESSAGES CONTROL] # W0511 = fixme # C0111 = missing docstring # C0103 = invalid name # I0011 = locally disabling # W0142 = star-args disable=W0511,C0111,C0103,I0011,W0142 gitless-0.8.8/.travis.sh000077500000000000000000000004101347331605400151270ustar00rootroot00000000000000#!/bin/sh cd ~ git clone --depth=1 -b maint/v0.28 https://github.com/libgit2/libgit2.git cd libgit2/ mkdir build && cd build cmake .. -DCMAKE_INSTALL_PREFIX=../_install -DBUILD_CLAR=OFF # don't build unit tests cmake --build . --target install ls -la .. cd ~ gitless-0.8.8/.travis.yml000066400000000000000000000014161347331605400153220ustar00rootroot00000000000000language: python dist: xenial addons: apt: packages: - snapcraft python: - '2.7' - '3.4' - '3.5' - '3.6' - '3.7' - 'pypy3.5' env: LIBGIT2=~/libgit2/_install/ LD_LIBRARY_PATH=~/libgit2/_install/lib before_install: ./.travis.sh install: pip install -r requirements.txt . before_script: - git config --global user.name "travis-test" - git config --global user.email "travis@test.com" script: - nosetests --logging-level=WARN - nosetests gitless/tests/test_e2e.py --logging-level=WARN branches: only: - master - develop jobs: include: - stage: Pack snap env: EMPTY before_install: skip install: skip script: snapcraft deploy: on: branch: master provider: snap snap: "*.snap" channel: edge skip_cleanup: true gitless-0.8.8/LICENSE.md000066400000000000000000000020541347331605400146140ustar00rootroot00000000000000Copyright (c) 2018 Santiago Perez De Rosso 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. gitless-0.8.8/MANIFEST.in000066400000000000000000000000561347331605400147460ustar00rootroot00000000000000include README.md LICENSE.md requirements.txt gitless-0.8.8/README.md000066400000000000000000000110231347331605400144630ustar00rootroot00000000000000Gitless ======= [![PyPI version](https://img.shields.io/pypi/v/gitless.svg)](https://pypi.org/project/gitless "PyPI version") [![Homebrew Formula](https://img.shields.io/homebrew/v/gitless.svg)](https://formulae.brew.sh/formula/gitless "Homebrew Formula") [![Snap Package](https://img.shields.io/badge/snap%20store-v0.8.7-orange.svg)](https://snapcraft.io/gitless) [![Travis Build Status](https://img.shields.io/travis/sdg-mit/gitless/master.svg)](https://travis-ci.org/sdg-mit/gitless "Travis Build Status") [![AppVeyor Build Status](https://ci.appveyor.com/api/projects/status/github/sdg-mit/gitless?svg=true)](https://ci.appveyor.com/project/spderosso/gitless "AppVeyor Build Status") [Gitless](http://gitless.com "Gitless's website") is an experimental version control system built on top of Git. Many people complain that Git is hard to use. We think the problem lies deeper than the user interface, in the concepts underlying Git. Gitless is an experiment to see what happens if you put a simple veneer on an app that changes the underlying concepts. Because Gitless is implemented on top of Git (could be considered what Git pros call a "porcelain" of Git), you can always fall back on Git. And of course your coworkers you share a repository with need never know that you're not a Git aficionado. Install ------- Note that the installation **won't interfere** with your Git installation in any way, you can keep using Git, and switch between Git and Gitless seamlessly. We currently require Git (1.7.12+) to be installed (but this requirement is going to disappear soon once we finish with our migration to [pygit2](https://github.com/libgit2/pygit2)). ### Installing from source To install from source you need to have Python (2.7, 3.2+ or PyPy) installed. Note to Windows users: Python 3 is not supported yet, see [#146](https://github.com/sdg-mit/gitless/issues/146) for more info. Additionally, you need to [install pygit2]( http://www.pygit2.org/install.html "pygit2 install"). Then, [download the source code tarball](http://gitless.com "Gitless's website") and do: $ ./setup.py install ### Installing via the Python Package Index If you are a Python fan you might find it easier to install Gitless via the Python Package Index. To do this, you need to have Python (2.7, 3.2+ or PyPy) installed. Note to Windows users: Python 3 is not supported yet, see [#146](https://github.com/sdg-mit/gitless/issues/146) for more info. Additionally, you need to [install pygit2]( http://www.pygit2.org/install.html "pygit2 install"). Then, just do: $ pip install gitless ### Installing via Homebrew (macOS only) If you are using [Homebrew](http://brew.sh/ "Homebrew homepage"), a package manager for macOS, you can install Gitless with: ``` brew update brew install gitless ``` ### Binary release (macOS only) A binary release for macOS is available from the [Gitless website](http://gitless.com "Gitless's website"). If you've downloaded a binary release of Gitless everything is contained in the gl binary, so to install simply do: $ cp path-to-downloaded-gl-binary /usr/local/bin/gl You can put the binary in other locations as well, just be sure to update your `PATH`. If for some reason this doesn't work (maybe you are running an old version of your OS?), try one of the other options (installing from source or via the Python Package Index). ### Installing via Snapcraft (Linux only) If you are using [Snapcraft](https://snapcraft.io/ "Snapcraft"), a package manager for Linux, you can install the most recent release of Gitless with: ``` snap install --channel=beta gitless ``` You can also use the `edge` channel to install the most recent build. Documentation ------------- `gl -h`, `gl subcmd -h` or check [our documentation](http://gitless.com "Gitless's website") Contribute ---------- If you find a bug, you can help us by submitting an issue to our GitHub Repository. If you'd like to contribute code, here are some useful things to know: - We follow (to some extent) the [Google Python Style Guide]( https://google.github.io/styleguide/pyguide.html "Google Python Style Guide"). Before submitting code, take a few seconds to look at the style guide and the Gitless's code so that your edits are consistent with the codebase - Finally, if you don't want [Travis]( https://travis-ci.org/sdg-mit/gitless "Travis") to be mad at you, check that tests pass in Python 2.7 and 3.2+. Tests can be run with: ``` pip install nose nosetests # run tests other than end-to-end tests nosetests ./gitless/tests/test_e2e.py # run end-to-end tests ``` gitless-0.8.8/gitless/000077500000000000000000000000001347331605400146615ustar00rootroot00000000000000gitless-0.8.8/gitless/__init__.py000066400000000000000000000000001347331605400167600ustar00rootroot00000000000000gitless-0.8.8/gitless/cli/000077500000000000000000000000001347331605400154305ustar00rootroot00000000000000gitless-0.8.8/gitless/cli/__init__.py000066400000000000000000000000001347331605400175270ustar00rootroot00000000000000gitless-0.8.8/gitless/cli/commit_dialog.py000066400000000000000000000046051347331605400206160ustar00rootroot00000000000000# -*- coding: utf-8 -*- # Gitless - a version control system built on top of Git # Licensed under MIT """Gitless's commit dialog.""" from __future__ import unicode_literals import io from locale import getpreferredencoding import os import subprocess import sys import shlex from . import pprint IS_PY2 = sys.version_info[0] == 2 ENCODING = getpreferredencoding() or 'utf-8' _COMMIT_FILE = 'GL_COMMIT_EDIT_MSG' _MERGE_MSG_FILE = 'MERGE_MSG' def show(files, repo): """Show the commit dialog. Args: files: files for pre-populating the dialog. repo: the repository. Returns: The commit msg. """ if IS_PY2: # wb because we use pprint to write cf = io.open(_commit_file(repo), mode='wb') else: cf = io.open(_commit_file(repo), mode='w', encoding=ENCODING) curr_b = repo.current_branch if curr_b.merge_in_progress or curr_b.fuse_in_progress: merge_msg = io.open( _merge_msg_file(repo), mode='r', encoding=ENCODING).read() cf.write(merge_msg) cf.write('\n') pprint.sep(stream=cf.write) pprint.msg( 'Please enter the commit message for your changes above, an empty ' 'message aborts', stream=cf.write) pprint.msg('the commit.', stream=cf.write) pprint.blank(stream=cf.write) pprint.msg( 'These are the files whose changes will be committed:', stream=cf.write) for f in files: pprint.item(f, stream=cf.write) pprint.sep(stream=cf.write) cf.close() _launch_editor(cf.name, repo) return _extract_msg(repo) def _launch_editor(fp, repo): try: editor = repo.config['core.editor'] except KeyError: editor = os.environ['EDITOR'] if 'EDITOR' in os.environ else 'vim' cmd = shlex.split(editor) cmd.append(fp) try: ret = subprocess.call(cmd) if ret != 0: pprint.err('Call to editor {0} failed'.format(editor)) except OSError: pprint.err('Couldn\'t launch editor {0}'.format(editor)) pprint.err_exp('change the value of git\'s core.editor setting') def _extract_msg(repo): cf = io.open(_commit_file(repo), mode='r', encoding=ENCODING) sep = pprint.SEP + '\n' msg = '' l = cf.readline() while l != sep and len(l) > 0: msg += l l = cf.readline() # We reached the separator, this marks the end of the commit msg return msg def _commit_file(repo): return os.path.join(repo.path, _COMMIT_FILE) def _merge_msg_file(repo): return os.path.join(repo.path, _MERGE_MSG_FILE) gitless-0.8.8/gitless/cli/file_cmd.py000066400000000000000000000026221347331605400175460ustar00rootroot00000000000000# -*- coding: utf-8 -*- # Gitless - a version control system built on top of Git # Licensed under MIT """Helper module for gl_{track, untrack, resolve}.""" from __future__ import unicode_literals from . import helpers, pprint VOWELS = ('a', 'e', 'i', 'o', 'u') def parser(help_msg, subcmd): def f(subparsers, repo): p = subparsers.add_parser( subcmd, help=help_msg, description=help_msg.capitalize()) p.add_argument( 'files', nargs='+', help='the file(s) to {0}'.format(subcmd), action=helpers.PathProcessor, repo=repo, skip_dir_test=repo and repo.current_branch.path_is_ignored, skip_dir_cb=lambda path: pprint.warn( 'Skipped files under directory {0} since they are all ' 'ignored'.format(path))) p.set_defaults(func=main(subcmd)) return f def main(subcmd): def f(args, repo): curr_b = repo.current_branch success = True for fp in args.files: try: getattr(curr_b, subcmd + '_file')(fp) pprint.ok( 'File {0} is now a{1} {2}{3}d file'.format( fp, 'n' if subcmd.startswith(VOWELS) else '', subcmd, '' if subcmd.endswith('e') else 'e')) except KeyError: pprint.err('Can\'t {0} non-existent file {1}'.format(subcmd, fp)) success = False except ValueError as e: pprint.err(e) success = False return success return f gitless-0.8.8/gitless/cli/gl.py000066400000000000000000000072461347331605400164150ustar00rootroot00000000000000# -*- coding: utf-8 -*- # Gitless - a version control system built on top of Git # Licensed under MIT """gl - Main Gitless's command. Dispatcher to the other cmds.""" from __future__ import unicode_literals import sys import argparse import traceback import pygit2 if sys.platform != 'win32': from sh import ErrorReturnCode else: from pbs import ErrorReturnCode from clint.textui import colored from gitless import core from . import ( gl_track, gl_untrack, gl_status, gl_diff, gl_commit, gl_branch, gl_tag, gl_checkout, gl_merge, gl_resolve, gl_fuse, gl_remote, gl_publish, gl_switch, gl_init, gl_history) from . import pprint SUCCESS = 0 ERRORS_FOUND = 1 # 2 is used by argparse to indicate cmd syntax errors. INTERNAL_ERROR = 3 NOT_IN_GL_REPO = 4 __version__ = '0.8.8' URL = 'http://gitless.com' repo = None try: repo = core.Repository() try: colored.DISABLE_COLOR = not repo.config.get_bool('color.ui') except pygit2.GitError: colored.DISABLE_COLOR = ( repo.config['color.ui'] in ['no', 'never']) except (core.NotInRepoError, KeyError): pass def print_help(parser): """print help for humans""" print(parser.description) print('\ncommands:\n') # https://stackoverflow.com/questions/20094215/argparse-subparser-monolithic-help-output # retrieve subparsers from parser subparsers_actions = [ action for action in parser._actions if isinstance(action, argparse._SubParsersAction)] # there will probably only be one subparser_action, # but better safe than sorry for subparsers_action in subparsers_actions: # get all subparsers and print help for choice in subparsers_action._choices_actions: print(' {:<19} {}'.format(choice.dest, choice.help)) def main(): parser = argparse.ArgumentParser( description=( 'Gitless: a version control system built on top of Git.\nMore info, ' 'downloads and documentation at {0}'.format(URL)), formatter_class=argparse.RawDescriptionHelpFormatter) parser.add_argument( '--version', action='version', version=( 'GL Version: {0}\nYou can check if there\'s a new version of Gitless ' 'available at {1}'.format(__version__, URL))) subparsers = parser.add_subparsers(title='subcommands', dest='subcmd_name') subparsers.required = True sub_cmds = [ gl_track, gl_untrack, gl_status, gl_diff, gl_commit, gl_branch, gl_tag, gl_checkout, gl_merge, gl_resolve, gl_fuse, gl_remote, gl_publish, gl_switch, gl_init, gl_history] for sub_cmd in sub_cmds: sub_cmd.parser(subparsers, repo) if len(sys.argv) == 1: print_help(parser) return SUCCESS args = parser.parse_args() try: if args.subcmd_name != 'init' and not repo: raise core.NotInRepoError('You are not in a Gitless\'s repository') return SUCCESS if args.func(args, repo) else ERRORS_FOUND except KeyboardInterrupt: pprint.puts('\n') pprint.msg('Keyboard interrupt detected, operation aborted') return SUCCESS except core.NotInRepoError as e: pprint.err(e) pprint.err_exp('do gl init to turn this directory into an empty repository') pprint.err_exp('do gl init remote_repo to clone an existing repository') return NOT_IN_GL_REPO except (ValueError, pygit2.GitError, core.GlError) as e: pprint.err(e) return ERRORS_FOUND except ErrorReturnCode as e: pprint.err(e.stderr) return ERRORS_FOUND except: pprint.err('Some internal error occurred') pprint.err_exp( 'If you want to help, see {0} for info on how to report bugs and ' 'include the following information:\n\n{1}\n\n{2}'.format( URL, __version__, traceback.format_exc())) return INTERNAL_ERROR gitless-0.8.8/gitless/cli/gl_branch.py000066400000000000000000000150701347331605400177240ustar00rootroot00000000000000# -*- coding: utf-8 -*- # Gitless - a version control system built on top of Git # Licensed under MIT """gl branch - List, create, edit or delete branches.""" from __future__ import unicode_literals from clint.textui import colored from gitless import core from . import helpers, pprint def parser(subparsers, _): """Adds the branch parser to the given subparsers object.""" desc = 'list, create, delete, or edit branches' branch_parser = subparsers.add_parser( 'branch', help=desc, description=desc.capitalize()) list_group = branch_parser.add_argument_group('list branches') list_group.add_argument( '-r', '--remote', help='list remote branches in addition to local branches', action='store_true') list_group.add_argument( '-v', '--verbose', help='be verbose, will output the head of each branch', action='store_true') create_group = branch_parser.add_argument_group('create branches') create_group.add_argument( '-c', '--create', nargs='+', help='create branch(es)', dest='create_b', metavar='branch') create_group.add_argument( '-dp', '--divergent-point', help='the commit from where to \'branch out\' (only relevant if a new ' 'branch is created; defaults to HEAD)', dest='dp') delete_group = branch_parser.add_argument_group('delete branches') delete_group.add_argument( '-d', '--delete', nargs='+', help='delete branch(es)', dest='delete_b', metavar='branch') edit_group = branch_parser.add_argument_group('edit the current branch') edit_group.add_argument( '-sh', '--set-head', help='set the head of the current branch', dest='new_head', metavar='commit_id') edit_group.add_argument( '-su', '--set-upstream', help='set the upstream branch of the current branch', dest='upstream_b', metavar='branch') edit_group.add_argument( '-uu', '--unset-upstream', help='unset the upstream branch of the current branch', action='store_true') branch_parser.set_defaults(func=main) def main(args, repo): is_list = bool(args.verbose or args.remote) is_create = bool(args.create_b or args.dp) is_delete = bool(args.delete_b) is_edit = bool(args.new_head or args.upstream_b or args.unset_upstream) if is_list + is_create + is_delete + is_edit > 1: pprint.err('Invalid flag combination') pprint.err_exp( 'Can only do one of list, create, delete, or edit branches at a time') return False ret = True if args.create_b: ret = _do_create(args.create_b, args.dp or 'HEAD', repo) elif args.delete_b: ret = _do_delete(args.delete_b, repo) elif args.upstream_b: ret = _do_set_upstream(args.upstream_b, repo) elif args.unset_upstream: ret = _do_unset_upstream(repo) elif args.new_head: ret = _do_set_head(args.new_head, repo) else: _do_list(repo, args.remote, v=args.verbose) return ret def _do_list(repo, list_remote, v=False): pprint.msg('List of branches:') pprint.exp('do gl branch -c b to create branch b') pprint.exp('do gl branch -d b to delete branch b') pprint.exp('do gl switch b to switch to branch b') pprint.exp('* = current branch') pprint.blank() for b in (repo.lookup_branch(n) for n in sorted(repo.listall_branches())): current_str = '*' if b.is_current else ' ' upstream_str = '(upstream is {0})'.format(b.upstream) if b.upstream else '' color = colored.green if b.is_current else colored.yellow pprint.item( '{0} {1} {2}'.format(current_str, color(b.branch_name), upstream_str)) if v: pprint.item(' ➜ head is {0}'.format(pprint.commit_str(b.head))) if list_remote: for r in sorted(repo.remotes, key=lambda r: r.name): for b in (r.lookup_branch(n) for n in sorted(r.listall_branches())): pprint.item(' {0}'.format(colored.yellow(str(b)))) if v: pprint.item(' ➜ head is {0}'.format(pprint.commit_str(b.head))) def _do_create(create_b, dp, repo): errors_found = False try: target = repo.revparse_single(dp) except KeyError: raise ValueError('Invalid divergent point {0}'.format(dp)) for b_name in create_b: r = repo remote_str = '' if '/' in b_name: # might want to create a remote branch maybe_remote, maybe_remote_branch = b_name.split('/', 1) if maybe_remote in repo.remotes: r = repo.remotes[maybe_remote] b_name = maybe_remote_branch conf_msg = 'Branch {0} will be created in remote repository {1}'.format( b_name, maybe_remote) if not pprint.conf_dialog(conf_msg): pprint.msg( 'Aborted: creation of branch {0} in remote repository {1}'.format( b_name, maybe_remote)) continue remote_str = ' in remote repository {0}'.format(maybe_remote) try: r.create_branch(b_name, target) pprint.ok('Created new branch {0}{1}'.format(b_name, remote_str)) except ValueError as e: pprint.err(e) errors_found = True return not errors_found def _do_delete(delete_b, repo): errors_found = False for b_name in delete_b: try: b = helpers.get_branch(b_name, repo) branch_str = 'Branch {0} will be removed'.format(b.branch_name) remote_str = '' if isinstance(b, core.RemoteBranch): remote_str = ' from remote repository {0}'.format(b.remote_name) if not pprint.conf_dialog('{0}{1}'.format(branch_str, remote_str)): pprint.msg('Aborted: removal of branch {0}'.format(b)) continue b.delete() pprint.ok('Branch {0} removed successfully'.format(b)) except ValueError as e: pprint.err(e) errors_found = True except core.BranchIsCurrentError as e: pprint.err(e) pprint.err_exp( 'do gl branch b to create or switch to another branch b and then ' 'gl branch -d {0} to remove branch {0}'.format(b)) errors_found = True return not errors_found def _do_set_upstream(upstream, repo): curr_b = repo.current_branch curr_b.upstream = helpers.get_branch(upstream, repo) pprint.ok('Current branch {0} set to track {1}'.format(curr_b, upstream)) return True def _do_unset_upstream(repo): curr_b = repo.current_branch curr_b.upstream = None pprint.ok('Upstream unset for current branch {0}'.format(curr_b)) return True def _do_set_head(commit_id, repo): try: commit = repo.revparse_single(commit_id) except KeyError: raise ValueError('Invalid head {0}'.format(commit_id)) curr_b = repo.current_branch curr_b.head = commit.id pprint.ok( 'Head of current branch {0} is now {1}'.format(curr_b, pprint.commit_str(commit))) return True gitless-0.8.8/gitless/cli/gl_checkout.py000066400000000000000000000037241347331605400202770ustar00rootroot00000000000000# -*- coding: utf-8 -*- # Gitless - a version control system built on top of Git # Licensed under MIT """gl checkout - Checkout committed versions of files.""" from __future__ import unicode_literals from gitless import core from . import helpers, pprint def parser(subparsers, repo): """Adds the checkout parser to the given subparsers object.""" desc = 'checkout committed versions of files' checkout_parser = subparsers.add_parser( 'checkout', help=desc, description=desc.capitalize()) checkout_parser.add_argument( '-cp', '--commit-point', help=( 'the commit point to checkout the files at. Defaults to HEAD.'), dest='cp', default='HEAD') checkout_parser.add_argument( 'files', nargs='+', help='the file(s) to checkout', action=helpers.PathProcessor, repo=repo, recursive=False) checkout_parser.set_defaults(func=main) def main(args, repo): errors_found = False curr_b = repo.current_branch cp = args.cp for fp in args.files: conf_msg = ( 'You have uncomitted changes in "{0}" that could be overwritten by ' 'checkout'.format(fp)) try: f = curr_b.status_file(fp) if f.type == core.GL_STATUS_TRACKED and f.modified and ( not pprint.conf_dialog(conf_msg)): pprint.err('Checkout aborted') continue except KeyError: pass try: curr_b.checkout_file(fp, repo.revparse_single(cp)) pprint.ok( 'File {0} checked out successfully to its state at {1}'.format( fp, cp)) except core.PathIsDirectoryError: commit = repo.revparse_single(cp) for fp in curr_b.get_paths(fp, commit): curr_b.checkout_file(fp, commit) pprint.ok( 'File {0} checked out successfully to its state at {1}'.format( fp, cp)) except KeyError: pprint.err('Checkout aborted') pprint.err('There\'s no file {0} at {1}'.format(fp, cp)) errors_found = True return not errors_found gitless-0.8.8/gitless/cli/gl_commit.py000066400000000000000000000067371347331605400177710ustar00rootroot00000000000000# -*- coding: utf-8 -*- # Gitless - a version control system built on top of Git # Licensed under MIT """gl commit - Record changes in the local repository.""" from __future__ import unicode_literals import subprocess import sys if sys.platform != 'win32': from sh import git else: from pbs import Command git = Command('git') from gitless import core from . import commit_dialog from . import helpers, pprint def parser(subparsers, repo): """Adds the commit parser to the given subparsers object.""" desc = 'save changes to the local repository' commit_parser = subparsers.add_parser( 'commit', help=desc, description=( desc.capitalize() + '. ' + 'By default all tracked modified files are committed. To customize the' ' set of files to be committed use the only, exclude, and include ' 'flags')) commit_parser.add_argument( '-m', '--message', help='Commit message', dest='m') commit_parser.add_argument( '-p', '--partial', help='Interactively select segments of files to commit', dest='p', action='store_true') helpers.oei_flags(commit_parser, repo) commit_parser.set_defaults(func=main) def main(args, repo): commit_files = helpers.oei_fs(args, repo) if not commit_files: pprint.err('No files to commit') pprint.err_exp('use gl track f if you want to track changes to file f') return False curr_b = repo.current_branch partials = None if args.p: partials = _do_partial_selection(commit_files, curr_b) if not _author_info_is_ok(repo): return False msg = args.m if args.m else commit_dialog.show(commit_files, repo) if not msg.strip(): if partials: git.reset('HEAD', partials) raise ValueError('Missing commit message') _auto_track(commit_files, curr_b) ci = curr_b.create_commit(commit_files, msg, partials=partials) pprint.ok('Commit on branch {0} succeeded'.format(repo.current_branch)) pprint.blank() pprint.commit(ci) if curr_b.fuse_in_progress: _op_continue(curr_b.fuse_continue, 'Fuse') elif curr_b.merge_in_progress: _op_continue(curr_b.merge_continue, 'Merge') return True def _author_info_is_ok(repo): def show_config_error(key): pprint.err('Missing {0} for commit author'.format(key)) pprint.err_exp('change the value of git\'s user.{0} setting'.format(key)) def config_is_ok(key): try: if not repo.config['user.{0}'.format(key)]: show_config_error(key) return False except KeyError: show_config_error(key) return False return True return config_is_ok('name') and config_is_ok('email') def _do_partial_selection(files, curr_b): partials = [] for fp in files: f_st = curr_b.status_file(fp) if not f_st.exists_at_head: pprint.warn('Can\'t select segments for new file {0}'.format(fp)) continue if not f_st.exists_in_wd: pprint.warn('Can\'t select segments for deleted file {0}'.format(fp)) continue subprocess.call(['git', 'add', '-p', fp]) # TODO: check that at least one hunk was staged partials.append(fp) return partials def _auto_track(files, curr_b): """Tracks those untracked files in the list.""" for fp in files: f = curr_b.status_file(fp) if f.type == core.GL_STATUS_UNTRACKED: curr_b.track_file(f.fp) def _op_continue(op, fn): pprint.blank() try: op(op_cb=pprint.OP_CB) pprint.ok('{0} succeeded'.format(fn)) except core.ApplyFailedError as e: pprint.ok('{0} succeeded'.format(fn)) raise e gitless-0.8.8/gitless/cli/gl_diff.py000066400000000000000000000031141347331605400173730ustar00rootroot00000000000000# -*- coding: utf-8 -*- # Gitless - a version control system built on top of Git # Licensed under MIT """gl diff - Show changes in files.""" from __future__ import unicode_literals import os import tempfile from . import helpers, pprint def parser(subparsers, repo): """Adds the diff parser to the given subparsers object.""" desc = 'show changes to files' diff_parser = subparsers.add_parser( 'diff', help=desc, description=( desc.capitalize() + '. ' + 'By default all tracked modified files are diffed. To customize the ' ' set of files to diff use the only, exclude, and include flags')) helpers.oei_flags(diff_parser, repo) diff_parser.set_defaults(func=main) def main(args, repo): files = helpers.oei_fs(args, repo) if not files: pprint.warn('No files to diff') success = True curr_b = repo.current_branch with tempfile.NamedTemporaryFile(mode='w', delete=False) as tf: for fp in files: try: patch = curr_b.diff_file(fp) except KeyError: pprint.err('Can\'t diff non-existent file {0}'.format(fp)) success = False continue if patch.delta.is_binary: pprint.warn('Not showing diffs for binary file {0}'.format(fp)) continue additions = patch.line_stats[1] deletions = patch.line_stats[2] if (not additions) and (not deletions): pprint.warn('No diffs to output for {0}'.format(fp)) continue pprint.diff(patch, stream=tf.write) if os.path.getsize(tf.name) > 0: helpers.page(tf.name, repo) os.remove(tf.name) return success gitless-0.8.8/gitless/cli/gl_fuse.py000066400000000000000000000064621347331605400174360ustar00rootroot00000000000000# -*- coding: utf-8 -*- # Gitless - a version control system built on top of Git # Licensed under MIT """gl fuse - Fuse the divergent changes of a branch onto the current branch.""" from __future__ import unicode_literals from gitless import core from . import helpers, pprint def parser(subparsers, repo): desc = 'fuse the divergent changes of a branch onto the current branch' fuse_parser = subparsers.add_parser( 'fuse', help=desc, description=( desc.capitalize() + '. ' + 'By default all divergent changes from the given source branch are ' 'fused. To customize the set of commits to fuse use the only and ' 'exclude flags')) fuse_parser.add_argument( 'src', nargs='?', help=( 'the source branch to read changes from. If none is given the upstream ' 'branch of the current branch is used as the source')) fuse_parser.add_argument( '-o', '--only', nargs='+', help=( 'fuse only the commits given (commits must belong to the set of ' 'divergent commits from the given src branch)'), metavar='commit_id', action=helpers.CommitIdProcessor, repo=repo) fuse_parser.add_argument( '-e', '--exclude', nargs='+', help=( 'exclude from the fuse the commits given (commits must belong to the ' 'set of divergent commits from the given src branch)'), metavar='commit_id', action=helpers.CommitIdProcessor, repo=repo) fuse_parser.add_argument( '-ip', '--insertion-point', nargs='?', help=( 'the divergent changes will be inserted after the commit given, dp for ' 'divergent point is the default'), metavar='commit_id') fuse_parser.add_argument( '-a', '--abort', help='abort the fuse in progress', action='store_true') fuse_parser.set_defaults(func=main) def main(args, repo): current_b = repo.current_branch if args.abort: current_b.abort_fuse(op_cb=pprint.OP_CB) pprint.ok('Fuse aborted successfully') return True src_branch = helpers.get_branch_or_use_upstream(args.src, 'src', repo) mb = repo.merge_base(current_b, src_branch) if mb == src_branch.target: # the current branch is ahead or both branches are equal pprint.err('No commits to fuse') return False if (not args.insertion_point or args.insertion_point == 'dp' or args.insertion_point == 'divergent-point'): insertion_point = mb else: insertion_point = repo.revparse_single(args.insertion_point).id def valid_input(inp): walker = src_branch.history() walker.hide(insertion_point) divergent_ids = frozenset(ci.id for ci in walker) errors_found = False for ci in inp - divergent_ids: pprint.err( 'Commit with id {0} is not among the divergent commits of branch ' '{1}'.format(ci, src_branch)) errors_found = True return not errors_found only = None exclude = None if args.only: only = frozenset(args.only) if not valid_input(only): return False elif args.exclude: exclude = frozenset(args.exclude) if not valid_input(exclude): return False try: current_b.fuse( src_branch, insertion_point, only=only, exclude=exclude, op_cb=pprint.OP_CB) pprint.ok('Fuse succeeded') except core.ApplyFailedError as e: pprint.ok('Fuse succeeded') raise e return True gitless-0.8.8/gitless/cli/gl_history.py000066400000000000000000000032371347331605400201720ustar00rootroot00000000000000# -*- coding: utf-8 -*- # Gitless - a version control system built on top of Git # Licensed under MIT """gl history - Show commit history.""" from __future__ import unicode_literals import os import tempfile from . import helpers, pprint def parser(subparsers, _): """Adds the history parser to the given subparsers object.""" desc = 'show commit history' history_parser = subparsers.add_parser( 'history', help=desc, description=desc.capitalize()) history_parser.add_argument( '-v', '--verbose', help='be verbose, will output the diffs of the commit', action='store_true') history_parser.add_argument( '-l', '--limit', help='limit number of commits displayed', type=int) history_parser.add_argument( '-c', '--compact', help='output history in a compact format', action='store_true', default=False) history_parser.add_argument( '-b', '--branch', nargs='?', metavar='branch_name', dest='b', help='the branch to show history of (defaults to the current branch)') history_parser.set_defaults(func=main) def main(args, repo): b = helpers.get_branch(args.b, repo) if args.b else repo.current_branch with tempfile.NamedTemporaryFile(mode='w', delete=False) as tf: count = 0 for ci in b.history(): if args.limit and count == args.limit: break pprint.commit(ci, compact=args.compact, stream=tf.write) if not args.compact: pprint.puts(stream=tf.write) if args.verbose and len(ci.parents) == 1: for patch in b.diff_commits(ci.parents[0], ci): pprint.diff(patch, stream=tf.write) count += 1 helpers.page(tf.name, repo) os.remove(tf.name) return True gitless-0.8.8/gitless/cli/gl_init.py000066400000000000000000000017731347331605400174370ustar00rootroot00000000000000# -*- coding: utf-8 -*- # Gitless - a version control system built on top of Git # Licensed under MIT """gl init - Create an empty repo or make a clone.""" from __future__ import unicode_literals import os from gitless import core from . import pprint def parser(subparsers, _): """Adds the init parser to the given subparsers object.""" desc = ( 'create an empty git repository or clone remote') init_parser = subparsers.add_parser( 'init', help=desc, description=desc.capitalize()) init_parser.add_argument( 'repo', nargs='?', help=( 'an optional remote repo address from where to read to create the ' 'local repo')) init_parser.set_defaults(func=main) def main(args, repo): if repo: pprint.err('You are already in a Gitless repository') return False core.init_repository(url=args.repo) pprint.ok('Local repo created in {0}'.format(os.getcwd())) if args.repo: pprint.ok('Initialized from remote {0}'.format(args.repo)) return True gitless-0.8.8/gitless/cli/gl_merge.py000066400000000000000000000022371347331605400175670ustar00rootroot00000000000000# -*- coding: utf-8 -*- # Gitless - a version control system built on top of Git # Licensed under MIT """gl merge - Merge the divergent changes of one branch onto another.""" from __future__ import unicode_literals from gitless import core from . import helpers, pprint def parser(subparsers, repo): desc = 'merge the divergent changes of one branch onto another' merge_parser = subparsers.add_parser( 'merge', help=desc, description=desc.capitalize()) group = merge_parser.add_mutually_exclusive_group() group.add_argument( 'src', nargs='?', help='the source branch to read changes from') group.add_argument( '-a', '--abort', help='abort the merge in progress', action='store_true') merge_parser.set_defaults(func=main) def main(args, repo): current_b = repo.current_branch if args.abort: current_b.abort_merge() pprint.ok('Merge aborted successfully') return True src_branch = helpers.get_branch_or_use_upstream(args.src, 'src', repo) try: current_b.merge(src_branch, op_cb=pprint.OP_CB) pprint.ok('Merge succeeded') except core.ApplyFailedError as e: pprint.ok('Merge succeeded') raise e return True gitless-0.8.8/gitless/cli/gl_publish.py000066400000000000000000000015511347331605400201340ustar00rootroot00000000000000# -*- coding: utf-8 -*- # Gitless - a version control system built on top of Git # Licensed under MIT """gl publish - Publish commits upstream.""" from __future__ import unicode_literals from . import helpers, pprint def parser(subparsers, _): """Adds the publish parser to the given subparsers object.""" desc = 'publish commits upstream' publish_parser = subparsers.add_parser( 'publish', help=desc, description=desc.capitalize()) publish_parser.add_argument( 'dst', nargs='?', help='the branch where to publish commits') publish_parser.set_defaults(func=main) def main(args, repo): current_b = repo.current_branch dst_b = helpers.get_branch_or_use_upstream(args.dst, 'dst', repo) current_b.publish(dst_b) pprint.ok( 'Publish of commits from branch {0} to branch {1} succeeded'.format( current_b, dst_b)) return True gitless-0.8.8/gitless/cli/gl_remote.py000066400000000000000000000042461347331605400177650ustar00rootroot00000000000000# -*- coding: utf-8 -*- # Gitless - a version control system built on top of Git # Licensed under MIT """gl remote - List, create, edit or delete remotes.""" from __future__ import unicode_literals from . import pprint def parser(subparsers, _): """Adds the remote parser to the given subparsers object.""" desc = 'list, create, edit or delete remotes' remote_parser = subparsers.add_parser( 'remote', help=desc, description=desc.capitalize()) remote_parser.add_argument( '-c', '--create', nargs='?', help='create remote', dest='remote_name', metavar='remote') remote_parser.add_argument( 'remote_url', nargs='?', help='the url of the remote (only relevant if a new remote is created)') remote_parser.add_argument( '-d', '--delete', nargs='+', help='delete remote(es)', dest='delete_r', metavar='remote') remote_parser.set_defaults(func=main) def main(args, repo): ret = True remotes = repo.remotes if args.remote_name: if not args.remote_url: raise ValueError('Missing url') ret = _do_create(args.remote_name, args.remote_url, remotes) elif args.delete_r: ret = _do_delete(args.delete_r, remotes) else: ret = _do_list(remotes) return ret def _do_list(remotes): pprint.msg('List of remotes:') pprint.exp( 'do gl remote -c r r_url to add a new remote r mapping to r_url') pprint.exp('do gl remote -d r to delete remote r') pprint.blank() if not len(remotes): pprint.item('There are no remotes to list') else: for r in remotes: pprint.item(r.name, opt_text=' (maps to {0})'.format(r.url)) return True def _do_create(rn, ru, remotes): remotes.create(rn, ru) pprint.ok('Remote {0} mapping to {1} created successfully'.format(rn, ru)) pprint.exp('to list existing remotes do gl remote') pprint.exp('to remove {0} do gl remote -d {1}'.format(rn, rn)) return True def _do_delete(delete_r, remotes): errors_found = False for r in delete_r: try: remotes.delete(r) pprint.ok('Remote {0} removed successfully'.format(r)) except KeyError: pprint.err('Remote \'{0}\' doesn\'t exist'.format(r)) errors_found = True return not errors_found gitless-0.8.8/gitless/cli/gl_resolve.py000066400000000000000000000004631347331605400201460ustar00rootroot00000000000000# -*- coding: utf-8 -*- # Gitless - a version control system built on top of Git # Licensed under MIT """gl resolve - Mark a file with conflicts as resolved.""" from __future__ import unicode_literals from . import file_cmd parser = file_cmd.parser('mark files with conflicts as resolved', 'resolve') gitless-0.8.8/gitless/cli/gl_status.py000066400000000000000000000077471347331605400200260ustar00rootroot00000000000000# -*- coding: utf-8 -*- # Gitless - a version control system built on top of Git # Licensed under MIT """gl status - Show the status of files in the repo.""" from __future__ import unicode_literals import os from clint.textui import colored from gitless import core from . import helpers, pprint def parser(subparsers, repo): """Adds the status parser to the given subparsers object.""" desc = 'show status of the repo' status_parser = subparsers.add_parser( 'status', help=desc, description=desc.capitalize()) status_parser.add_argument( 'paths', nargs='*', help='the specific path(s) to status', action=helpers.PathProcessor, repo=repo) status_parser.set_defaults(func=main) def main(args, repo): curr_b = repo.current_branch pprint.msg('On branch {0}, repo-directory {1}'.format( colored.green(curr_b.branch_name), colored.green('//' + repo.cwd))) if curr_b.merge_in_progress: pprint.blank() _print_conflict_exp('merge') elif curr_b.fuse_in_progress: pprint.blank() _print_conflict_exp('fuse') tracked_mod_list = [] untracked_list = [] paths = frozenset(args.paths) for f in curr_b.status(): if paths and (f.fp not in paths): continue if f.type == core.GL_STATUS_TRACKED and f.modified: tracked_mod_list.append(f) elif f.type == core.GL_STATUS_UNTRACKED: untracked_list.append(f) relative_paths = True # git seems to default to true try: relative_paths = repo.config.get_bool('status.relativePaths') except KeyError: pass pprint.blank() tracked_mod_list.sort(key=lambda f: f.fp) _print_tracked_mod_files(tracked_mod_list, relative_paths, repo) pprint.blank() pprint.blank() untracked_list.sort(key=lambda f: f.fp) _print_untracked_files(untracked_list, relative_paths, repo) return True def _print_tracked_mod_files(tracked_mod_list, relative_paths, repo): pprint.msg('Tracked files with modifications:') pprint.exp('these will be automatically considered for commit') pprint.exp( 'use gl untrack f if you don\'t want to track changes to file f') pprint.exp( 'if file f was committed before, use gl checkout f to discard ' 'local changes') pprint.blank() if not tracked_mod_list: pprint.item('There are no tracked files with modifications to list') return root = repo.root for f in tracked_mod_list: exp = '' color = colored.yellow if not f.exists_at_head: exp = ' (new file)' color = colored.green elif not f.exists_in_wd: exp = ' (deleted)' color = colored.red elif f.in_conflict: exp = ' (with conflicts)' color = colored.cyan fp = os.path.relpath(os.path.join(root, f.fp)) if relative_paths else f.fp if fp == '.': continue pprint.item(color(fp), opt_text=exp) def _print_untracked_files(untracked_list, relative_paths, repo): pprint.msg('Untracked files:') pprint.exp('these won\'t be considered for commit') pprint.exp('use gl track f if you want to track changes to file f') pprint.blank() if not untracked_list: pprint.item('There are no untracked files to list') return root = repo.root for f in untracked_list: exp = '' color = colored.blue if f.in_conflict: exp = ' (with conflicts)' color = colored.cyan elif f.exists_at_head: color = colored.magenta if f.exists_in_wd: exp = ' (exists at head)' else: exp = ' (exists at head but not in working directory)' fp = os.path.relpath(os.path.join(root, f.fp)) if relative_paths else f.fp if fp == '.': continue pprint.item(color(fp), opt_text=exp) def _print_conflict_exp(op): pprint.msg( 'You are in the middle of a {0}; all conflicts must be resolved before ' 'commiting'.format(op)) pprint.exp( 'use gl {0} --abort to go back to the state before the {0}'.format(op)) pprint.exp('use gl resolve f to mark file f as resolved') pprint.exp('once you solved all conflicts do gl commit to continue') pprint.blank() gitless-0.8.8/gitless/cli/gl_switch.py000066400000000000000000000021661347331605400177720ustar00rootroot00000000000000# -*- coding: utf-8 -*- # Gitless - a version control system built on top of Git # Licensed under MIT """gl switch - Switch branches.""" from __future__ import unicode_literals from . import pprint def parser(subparsers, _): """Adds the switch parser to the given subparsers object.""" desc = 'switch branches' switch_parser = subparsers.add_parser( 'switch', help=desc, description=desc.capitalize()) switch_parser.add_argument('branch', help='switch to branch') switch_parser.add_argument( '-mo', '--move-over', help='move uncomitted changes made in the current branch to the ' 'destination branch', action='store_true') switch_parser.set_defaults(func=main) def main(args, repo): b = repo.lookup_branch(args.branch) if not b: pprint.err('Branch {0} doesn\'t exist'.format(args.branch)) pprint.err_exp('to list existing branches do gl branch') pprint.err_exp('to create a new branch do gl branch -c {0}'.format(args.branch)) return False repo.switch_current_branch(b, move_over=args.move_over) pprint.ok('Switched to branch {0}'.format(args.branch)) return True gitless-0.8.8/gitless/cli/gl_tag.py000066400000000000000000000101211347331605400172320ustar00rootroot00000000000000# -*- coding: utf-8 -*- # Gitless - a version control system built on top of Git # Licensed under MIT """gl tag - List, create, edit or delete tags.""" from __future__ import unicode_literals from gitless import core from . import helpers, pprint def parser(subparsers, _): """Adds the tag parser to the given subparsers object.""" desc = 'list, create, or delete tags' tag_parser = subparsers.add_parser( 'tag', help=desc, description=desc.capitalize()) list_group = tag_parser.add_argument_group('list tags') list_group.add_argument( '-r', '--remote', help='list remote tags in addition to local tags', action='store_true') create_group = tag_parser.add_argument_group('create tags') create_group.add_argument( '-c', '--create', nargs='+', help='create tag(s)', dest='create_t', metavar='tag') create_group.add_argument( '-ci', '--commit', help='the commit to tag (only relevant if a new ' 'tag is created; defaults to the HEAD commit)', dest='ci') delete_group = tag_parser.add_argument_group('delete tags') delete_group.add_argument( '-d', '--delete', nargs='+', help='delete tag(s)', dest='delete_t', metavar='tag') tag_parser.set_defaults(func=main) def main(args, repo): is_list = bool(args.remote) is_create = bool(args.create_t or args.ci) is_delete = bool(args.delete_t) if is_list + is_create + is_delete > 1: pprint.err('Invalid flag combination') pprint.err_exp('Can only do one of list, create, or delete tags at a time') return False ret = True if args.create_t: ret = _do_create(args.create_t, args.ci or 'HEAD', repo) elif args.delete_t: ret = _do_delete(args.delete_t, repo) else: _do_list(repo, args.remote) return ret def _do_list(repo, list_remote): pprint.msg('List of tags:') pprint.exp('do gl tag -c t to create tag t') pprint.exp('do gl tag -d t to delete tag t') pprint.blank() no_tags = True for t in (repo.lookup_tag(n) for n in sorted(repo.listall_tags())): pprint.item('{0} ➜ tags {1}'.format(t, pprint.commit_str(t.commit))) no_tags = False if list_remote: for r in sorted(repo.remotes, key=lambda r: r.name): for t in (r.lookup_tag(n) for n in sorted(r.listall_tags())): pprint.item('{0} ➜ tags {1}'.format(t, pprint.commit_str(t.commit))) no_tags = False if no_tags: pprint.item('There are no tags to list') def _do_create(create_t, dp, repo): errors_found = False try: target = repo.revparse_single(dp) except KeyError: raise ValueError('Invalid commit {0}'.format(dp)) for t_name in create_t: r = repo remote_str = '' if '/' in t_name: # might want to create a remote tag maybe_remote, maybe_remote_tag = t_name.split('/', 1) if maybe_remote in repo.remotes: r = repo.remotes[maybe_remote] t_name = maybe_remote_tag conf_msg = 'Tag {0} will be created in remote repository {1}'.format( t_name, maybe_remote) if not pprint.conf_dialog(conf_msg): pprint.msg( 'Aborted: creation of tag {0} in remote repository {1}'.format( t_name, maybe_remote)) continue remote_str = ' in remote repository {0}'.format(maybe_remote) try: r.create_tag(t_name, target) pprint.ok('Created new tag {0}{1}'.format(t_name, remote_str)) except ValueError as e: pprint.err(e) errors_found = True return not errors_found def _do_delete(delete_t, repo): errors_found = False for t_name in delete_t: try: t = helpers.get_tag(t_name, repo) tag_str = 'Tag {0} will be removed'.format(t.tag_name) remote_str = '' if isinstance(t, core.RemoteTag): remote_str = 'from remote repository {0}'.format(t.remote_name) if not pprint.conf_dialog('{0} {1}'.format(tag_str, remote_str)): pprint.msg('Aborted: removal of tag {0}'.format(t)) continue t.delete() pprint.ok('Tag {0} removed successfully'.format(t)) except ValueError as e: pprint.err(e) errors_found = True return not errors_found gitless-0.8.8/gitless/cli/gl_track.py000066400000000000000000000004421347331605400175700ustar00rootroot00000000000000# -*- coding: utf-8 -*- # Gitless - a version control system built on top of Git # Licensed under MIT """gl track - Start tracking changes to files.""" from __future__ import unicode_literals from . import file_cmd parser = file_cmd.parser('start tracking changes to files', 'track') gitless-0.8.8/gitless/cli/gl_untrack.py000066400000000000000000000004441347331605400201350ustar00rootroot00000000000000# -*- coding: utf-8 -*- # Gitless - a version control system built on top of Git # Licensed under MIT """gl untrack - Stop tracking changes to files.""" from __future__ import unicode_literals from . import file_cmd parser = file_cmd.parser('stop tracking changes to files', 'untrack') gitless-0.8.8/gitless/cli/helpers.py000066400000000000000000000175111347331605400174510ustar00rootroot00000000000000# -*- coding: utf-8 -*- # Gitless - a version control system built on top of Git # Licensed under MIT """Some helpers for commands.""" from __future__ import unicode_literals import argparse import os import subprocess import sys import shlex import shutil from gitless import core from . import pprint def get_branch(branch_name, repo): return _get_ref("branch", branch_name, repo) def get_tag(tag_name, repo): return _get_ref("tag", tag_name, repo) def _get_ref(ref_type, ref_name, repo): ref_type_cap = ref_type.capitalize() r = getattr(repo, "lookup_" + ref_type)(ref_name) if not r: if '/' not in ref_name: raise ValueError( '{0} "{1}" doesn\'t exist'.format(ref_type_cap, ref_name)) # It might be a remote ref remote, remote_ref = ref_name.split('/', 1) try: remote_repo = repo.remotes[remote] except KeyError: raise ValueError( 'Remote "{0}" doesn\'t exist, and there is no local ' '{1} "{2}"'.format(remote, ref_type_cap, ref_name)) r = getattr(remote_repo, "lookup_" + ref_type)(remote_ref) if not r: raise ValueError('{0} "{1}" doesn\'t exist in remote "{2}"'.format( ref_type_cap, remote_ref, remote)) return r def get_branch_or_use_upstream(branch_name, arg, repo): if not branch_name: # use upstream branch current_b = repo.current_branch upstream_b = current_b.upstream if not upstream_b: raise ValueError( 'No {0} branch specified and the current branch has no upstream ' 'branch set'.format(arg)) ret = current_b.upstream else: ret = get_branch(branch_name, repo) return ret def page(fp, repo): if not sys.stdout.isatty(): # we are being piped or redirected if sys.platform != 'win32': # Prevent Python from throwing exceptions on SIGPIPE from signal import signal, SIGPIPE, SIG_DFL signal(SIGPIPE, SIG_DFL) # memory-friendly way to output contents of file to stdout with open(fp, 'r') as f: shutil.copyfileobj(f, sys.stdout) return # On Windows, we need to call 'more' through cmd.exe (with 'cmd'). The /C is # so that the command window gets closed after 'more' finishes default_pager = 'less' if sys.platform != 'win32' else 'cmd /C more' try: pager = repo.config['core.pager'] except KeyError: pager = '' # empty string will evaluate to False below pager = pager or os.environ.get('PAGER', None) or default_pager cmd = shlex.split(pager) # split into constituents if os.path.basename(cmd[0]) == 'less': cmd.extend(['-r', '-f']) # append arguments cmd.append(fp) # add file name to page command try: ret = subprocess.call(cmd, stdin=sys.stdin, stdout=sys.stdout) if ret != 0: pprint.err('Call to pager {0} failed'.format(pager)) except OSError: pprint.err('Couldn\'t launch pager {0}'.format(pager)) pprint.err_exp('change the value of git\'s core.pager setting') class PathProcessor(argparse.Action): def __init__( self, option_strings, dest, repo=None, skip_dir_test=None, skip_dir_cb=None, recursive=True, **kwargs): self.repo = repo self.skip_dir_test = skip_dir_test self.skip_dir_cb = skip_dir_cb self.recursive = recursive super(PathProcessor, self).__init__(option_strings, dest, **kwargs) def __call__(self, parser, namespace, paths, option_string=None): root = self.repo.root if self.repo else '' repo_dir = self.repo.path[:-1] if self.repo else '' # strip trailing / def process_paths(): for path in paths: path = os.path.abspath(path) if self.recursive and os.path.isdir(path): for curr_dir, dirs, fps in os.walk(path, topdown=True): if curr_dir.startswith(repo_dir): dirs[:] = [] continue curr_dir_rel = os.path.relpath(curr_dir, root) if (curr_dir_rel != "." and self.skip_dir_test and self.skip_dir_test(curr_dir_rel)): if self.skip_dir_cb: self.skip_dir_cb(curr_dir_rel) dirs[:] = [] continue for fp in fps: yield os.path.join(curr_dir_rel, fp) else: if not path.startswith(repo_dir): yield os.path.relpath(path, root) else: yield path setattr(namespace, self.dest, process_paths()) class CommitIdProcessor(argparse.Action): def __init__(self, option_strings, dest, repo=None, **kwargs): self.repo = repo super(CommitIdProcessor, self).__init__(option_strings, dest, **kwargs) def __call__(self, parser, namespace, revs, option_string=None): cids = (self.repo.revparse_single(rev).id for rev in revs) setattr(namespace, self.dest, cids) def oei_flags(subparsers, repo): subparsers.add_argument( 'only', nargs='*', help='use only files given (tracked modified or untracked)', action=PathProcessor, repo=repo, metavar='file') subparsers.add_argument( '-e', '--exclude', nargs='+', help='exclude files given (files must be tracked modified)', action=PathProcessor, repo=repo, metavar='file') subparsers.add_argument( '-i', '--include', nargs='+', help='include files given (files must be untracked)', action=PathProcessor, repo=repo, metavar='file') def oei_fs(args, repo): """Compute the final fileset per oei flags.""" only = frozenset(args.only if args.only else []) exclude = frozenset(args.exclude if args.exclude else []) include = frozenset(args.include if args.include else []) curr_b = repo.current_branch if not _oei_validate(only, exclude, include, curr_b): raise ValueError('Invalid input') if only: ret = only else: # Tracked modified files ret = frozenset( f.fp for f in curr_b.status() if f.type == core.GL_STATUS_TRACKED and f.modified) # using generator expression # We get the files from status with forward slashes. On Windows, these # won't match the paths provided by the user, which are normalized by # PathProcessor if sys.platform == 'win32': ret = frozenset(p.replace('/', '\\') for p in ret) ret -= exclude ret |= include ret = sorted(list(ret)) return ret def _oei_validate(only, exclude, include, curr_b): """Validates user input per oei flags. This function will print to stderr in case user-provided values are invalid (and return False). Returns: True if the input is valid, False if otherwise. """ if only and (exclude or include): pprint.err( 'You provided a list of filenames to be committed but also ' 'provided a list of files to be excluded (-e) or included (-i)') return False err = [] def validate(fps, check_fn, msg): ''' fps: files check_fn: lambda(file) -> boolean msg: string-format of pre-defined constant string. ''' ret = True if not fps: return ret for fp in fps: try: f = curr_b.status_file(fp) except KeyError: err.append('File {0} doesn\'t exist'.format(fp)) ret = False # set error flag, but keep assessing other files else: # executed after "try", exception will be ignored here if not check_fn(f): err.append(msg(fp)) # dynamic string formatting ret = False return ret only_valid = validate( only, lambda f: f.type == core.GL_STATUS_UNTRACKED or ( f.type == core.GL_STATUS_TRACKED and f.modified), 'File {0} is not a tracked modified or untracked file'.format) exclude_valid = validate( exclude, lambda f: f.type == core.GL_STATUS_TRACKED and f.modified, 'File {0} is not a tracked modified file'.format) include_valid = validate( include, lambda f: f.type == core.GL_STATUS_UNTRACKED, 'File {0} is not an untracked file'.format) if only_valid and exclude_valid and include_valid: return True for e in err: pprint.err(e) return False gitless-0.8.8/gitless/cli/pprint.py000066400000000000000000000233431347331605400173230ustar00rootroot00000000000000# -*- coding: utf-8 -*- # Gitless - a version control system built on top of Git # Licensed under MIT """Module for pretty printing Gitless output.""" from __future__ import unicode_literals try: from StringIO import StringIO except ImportError: from io import StringIO from datetime import datetime, tzinfo, timedelta from locale import getpreferredencoding import re import sys from clint.textui import colored, indent from clint.textui import puts as clint_puts from gitless import core SEP = ( '##########################################################################' '######') IS_PY2 = sys.version_info[0] == 2 ENCODING = getpreferredencoding() or 'utf-8' def puts(s='', newline=True, stream=sys.stdout.write): assert not IS_PY2 or ( isinstance(s, unicode) or isinstance(s, colored.ColoredString)) if IS_PY2: s = s.encode(ENCODING, errors='ignore') clint_puts(s, newline=newline, stream=stream) # Stdout def ok(text): puts(colored.green('✔ {0}'.format(text))) def warn(text): puts(colored.yellow('! {0}'.format(text))) def msg(text, stream=sys.stdout.write): puts(text, stream=stream) def exp(text, stream=sys.stdout.write): with indent(2): puts('➜ {0}'.format(text), stream=stream) def item(i, opt_text='', stream=sys.stdout.write): with indent(4): puts('{0}{1}'.format(i, opt_text), stream=stream) def blank(stream=sys.stdout.write): puts('', stream=stream) def sep(stream=sys.stdout.write): puts(SEP, stream=stream) # Err def err(text): puts(colored.red('✘ {0}'.format(text)), stream=sys.stderr.write) def err_msg(text): msg(text, stream=sys.stderr.write) def err_exp(text): exp(text, stream=sys.stderr.write) def err_blank(): blank(stream=sys.stderr.write) def err_item(i, opt_text=''): item(i, opt_text, stream=sys.stderr.write) # Misc def conf_dialog(text): """Gets confirmation from the user. Prints a confirmation message to stdout with the given text and waits for user confirmation. Args: text: the text to include in the confirmation. Returns: True if the user confirmed she wanted to continue or False if otherwise. """ msg('{0}. Do you wish to continue? (y/N)'.format(text)) user_input = get_user_input() return user_input and user_input[0].lower() == 'y' def get_user_input(text='> '): """Python 2/3 compatible way of getting user input.""" global input try: # Disable pylint's redefined-builtin warning and undefined-variable # (raw_input is undefined in python 3) error. # pylint: disable=W0622 # pylint: disable=E0602 input = raw_input except NameError: pass return input(text) def commit_str(ci): ci_str = StringIO() commit(ci, compact=True, stream=ci_str.write) return ci_str.getvalue().strip() def commit(ci, compact=False, stream=sys.stdout.write): merge_commit = len(ci.parent_ids) > 1 color = colored.magenta if merge_commit else colored.yellow if compact: title = ci.message.splitlines()[0] puts('{0} {1}'.format(color(str(ci.id)[:7]), title), stream=stream) return puts(color('Commit Id: {0}'.format(ci.id)), stream=stream) if merge_commit: merges_str = ' '.join(str(oid)[:7] for oid in ci.parent_ids) puts(color('Merges: {0}'.format(merges_str)), stream=stream) puts( color('Author: {0} <{1}>'.format(ci.author.name, ci.author.email)), stream=stream) ci_author_dt = datetime.fromtimestamp( ci.author.time, FixedOffset(ci.author.offset)) puts(color('Date: {0:%c %z}'.format(ci_author_dt)), stream=stream) puts(stream=stream) with indent(4): puts(ci.message, stream=stream) # Op Callbacks def apply_ok(ci): ok('Insertion of {0} succeeded'.format(ci.id)) blank() commit(ci) blank() def apply_err(ci): err('Insertion of {0} failed'.format(ci.id)) blank() commit(ci) blank() def save(): warn('Temporarily saving uncommitted changes') def restore_ok(): ok('Uncommitted changes applied successfully to the new head of the branch') OP_CB = core.OpCb(apply_ok, apply_err, save, restore_ok) class FixedOffset(tzinfo): def __init__(self, offset): super(FixedOffset, self).__init__() self.__offset = timedelta(minutes=offset) def utcoffset(self, _): return self.__offset def dst(self, _): return timedelta(0) def diff(patch, stream=sys.stdout.write): # Diff header old_fp = patch.delta.old_file.path new_fp = patch.delta.new_file.path puts('Diff of file "{0}"'.format(old_fp), stream=stream) if old_fp != new_fp: puts(colored.cyan(' (renamed to {0})'.format(new_fp)), stream=stream) puts(stream=stream) if patch.delta.is_binary: puts('Not showing diffs for binary file', stream=stream) return additions = patch.line_stats[1] deletions = patch.line_stats[2] if (not additions) and (not deletions): puts('No diffs to output for file', stream=stream) return put_s = lambda num: '' if num == 1 else 's' puts('{0} line{1} added'.format(additions, put_s(additions)), stream=stream) puts('{0} line{1} removed'.format(deletions, put_s(deletions)), stream=stream) puts(stream=stream) # Diff body for hunk in patch.hunks: puts(stream=stream) _hunk(hunk, stream=stream) puts(stream=stream) puts(stream=stream) def _hunk(hunk, stream=sys.stdout.write): puts(colored.cyan('@@ -{0},{1} +{2},{3} @@'.format( hunk.old_start, hunk.old_lines, hunk.new_start, hunk.new_lines)), stream=stream) padding = _padding(hunk) del_line, add_line, maybe_bold, saw_add = None, None, False, False for diff_line in hunk.lines: assert not IS_PY2 or isinstance(diff_line.content, unicode) st = diff_line.origin if st == '-' and not maybe_bold: maybe_bold = True del_line = diff_line elif st == '+' and maybe_bold and not saw_add: saw_add = True add_line = diff_line elif st == ' ' and maybe_bold and saw_add: bold1, bold2 = _highlight(del_line.content, add_line.content) puts(_format_line(del_line, padding, bold_delim=bold1), stream=stream) puts(_format_line(add_line, padding, bold_delim=bold2), stream=stream) del_line, add_line, maybe_bold, saw_add = None, None, False, False puts(_format_line(diff_line, padding), stream=stream) else: if del_line: puts(_format_line(del_line, padding), stream=stream) if add_line: puts(_format_line(add_line, padding), stream=stream) del_line, add_line, maybe_bold, saw_add = None, None, False, False puts(_format_line(diff_line, padding), stream=stream) if maybe_bold and saw_add: bold1, bold2 = _highlight(del_line.content, add_line.content) puts(_format_line(del_line, padding, bold_delim=bold1), stream=stream) puts(_format_line(add_line, padding, bold_delim=bold2), stream=stream) else: if del_line: puts(_format_line(del_line, padding), stream=stream) if add_line: puts(_format_line(add_line, padding), stream=stream) def _padding(hunk): MIN_LINE_PADDING = 8 max_line_number = max([ hunk.old_start + hunk.old_lines, hunk.new_start + hunk.new_lines]) max_line_digits = len(str(max_line_number)) return max(MIN_LINE_PADDING, max_line_digits + 1) def _format_line(diff_line, padding, bold_delim=None): """Format a standard diff line. Returns: a padded and colored version of the diff line with line numbers """ # Color constants # We only output colored lines if the coloring is enabled and we are not being # piped or redirected if colored.DISABLE_COLOR or not sys.stdout.isatty(): GREEN = '' GREEN_BOLD = '' RED = '' RED_BOLD = '' CLEAR = '' else: GREEN = '\033[32m' GREEN_BOLD = '\033[1;32m' RED = '\033[31m' RED_BOLD = '\033[1;31m' CLEAR = '\033[0m' formatted = '' st = diff_line.origin line = st + diff_line.content.rstrip('\n') old_lineno = diff_line.old_lineno new_lineno = diff_line.new_lineno if st == ' ': formatted = ( str(old_lineno).ljust(padding) + str(new_lineno).ljust(padding) + line) elif st == '+': formatted = ' ' * padding + GREEN + str(new_lineno).ljust(padding) if not bold_delim: formatted += line else: bold_start, bold_end = bold_delim formatted += ( line[:bold_start] + GREEN_BOLD + line[bold_start:bold_end] + CLEAR + GREEN + line[bold_end:]) elif st == '-': formatted = RED + str(old_lineno).ljust(padding) + ' ' * padding if not bold_delim: formatted += line else: bold_start, bold_end = bold_delim formatted += ( line[:bold_start] + RED_BOLD + line[bold_start:bold_end] + CLEAR + RED + line[bold_end:]) return formatted + CLEAR def _highlight(line1, line2): """Returns the sections that should be bolded in the given lines. Returns: two tuples. Each tuple indicates the start and end of the section of the line that should be bolded for line1 and line2 respectively. """ start1 = start2 = 0 match = re.search(r'\S', line1) # ignore leading whitespace if match: start1 = match.start() match = re.search(r'\S', line2) if match: start2 = match.start() length = min(len(line1), len(line2)) - 1 bold_start1 = start1 bold_start2 = start2 while (bold_start1 <= length and bold_start2 <= length and line1[bold_start1] == line2[bold_start2]): bold_start1 += 1 bold_start2 += 1 match = re.search(r'\s*$', line1) # ignore trailing whitespace bold_end1 = match.start() - 1 match = re.search(r'\s*$', line2) bold_end2 = match.start() - 1 while (bold_end1 >= bold_start1 and bold_end2 >= bold_start2 and line1[bold_end1] == line2[bold_end2]): bold_end1 -= 1 bold_end2 -= 1 if bold_start1 - start1 > 0 or len(line1) - 1 - bold_end1 > 0: return (bold_start1 + 1, bold_end1 + 2), (bold_start2 + 1, bold_end2 + 2) return None, None gitless-0.8.8/gitless/core.py000066400000000000000000001245611347331605400161740ustar00rootroot00000000000000# -*- coding: utf-8 -*- # Gitless - a version control system built on top of Git # Licensed under MIT """Gitless's library.""" from __future__ import unicode_literals import collections import errno import io try: from itertools import izip as zip except ImportError: pass import itertools import json from locale import getpreferredencoding import os import re import shutil import pygit2 import sys if sys.platform != 'win32': from sh import git, ErrorReturnCode else: from pbs import Command, ErrorReturnCode git = Command('git') git = git.bake('--no-pager') ENCODING = getpreferredencoding() or 'utf-8' # Errors class GlError(Exception): pass class NotInRepoError(GlError): pass class BranchIsCurrentError(GlError): pass class ApplyFailedError(GlError): pass class PathIsDirectoryError(ValueError): pass # File status GL_STATUS_UNTRACKED = 1 GL_STATUS_TRACKED = 2 GL_STATUS_IGNORED = 3 def error_on_none(path): """Raise a KeyError if the ```path``` argument is None.""" if path is None: raise KeyError('path') return path def init_repository(url=None): """Creates a new Gitless's repository in the cwd. Args: url: if given the local repository will be a clone of the remote repository given by this url. """ cwd = os.getcwd() try: error_on_none(pygit2.discover_repository(cwd)) raise GlError('You are already in a Gitless repository') except KeyError: # Expected if not url: repo = pygit2.init_repository(cwd) # We also create an initial root commit git.commit(allow_empty=True, m='Initialize repository') return repo try: git.clone(url, cwd) except ErrorReturnCode as e: raise GlError(stderr(e)) # We get all remote branches as well and create local equivalents repo = Repository() remote = repo.remotes['origin'] for rb in (remote.lookup_branch(bn) for bn in remote.listall_branches()): if rb.branch_name == 'master': continue new_b = repo.create_branch(rb.branch_name, rb.head) new_b.upstream = rb return repo class Repository(object): """A Gitless's repository. Attributes: path: absolute path to the Gitless's dir (the .git dir). root: absolute path to the root of this repository. cwd: the current working directory relative to the root of this repository ('' if they are equal). config: the repository's configuration. current_branch: the current branch (a Branch object). remotes: the configured remotes (see RemoteCollection). """ def __init__(self): """Create a Repository out of the current working repository.""" try: path = error_on_none(pygit2.discover_repository(os.getcwd())) except KeyError: raise NotInRepoError('You are not in a Gitless\'s repository') self.git_repo = pygit2.Repository(path) self.remotes = RemoteCollection(self.git_repo.remotes, self) self.path = self.git_repo.path self.root = self.path[:-6] # strip trailing /.git/ self.config = self.git_repo.config @property def cwd(self): ret = os.path.relpath(os.getcwd(), self.root) return '' if ret == '.' else ret def revparse_single(self, revision): if '/' in revision: # might be a remote branch remote, remote_branch = revision.split('/', 1) try: return self.remotes[remote].lookup_branch(remote_branch).head except KeyError: pass try: return self.git_repo.revparse_single(revision) except (KeyError, ValueError): raise ValueError('No commit found for {0}'.format(revision)) def merge_base(self, b1, b2): try: return self.git_repo.merge_base(b1.target, b2.target) except KeyError: raise GlError('No common commit found between {0} and {1}'.format(b1, b2)) def _fuse_commits_fp(self, b): return os.path.join( self.path, 'GL_FUSE_CIS_{0}'.format(b.branch_name.replace('/', '_'))) def _ref_exists(self, ref): try: self.git_repo.lookup_reference(ref) return True except KeyError: return False def _ref_rm(self, ref): ref_path = os.path.join(self.path, ref) if os.path.exists(ref_path): os.remove(ref_path) def _ref_create(self, ref, value): ref_path = os.path.join(self.path, ref) with io.open(ref_path, 'w', encoding=ENCODING) as f: if value.startswith('refs'): value = 'ref: ' + value f.write(value + '\n') def _ref_target(self, ref): return self.git_repo.lookup_reference(ref).target # Tag-related methods def create_tag(self, name, commit): tagger = self.git_repo.default_signature try: self.git_repo.create_tag( name, commit.id, pygit2.GIT_OBJ_COMMIT, tagger, "") return Tag(name, commit) except ValueError as e: raise ValueError( str(e).replace('refs/tags/', '').replace('reference', 'tag')) def lookup_tag(self, tag_name): try: # We peel to get the commit pointed to by the tag (in the case of annotated # tags, lookup_reference returns a tag object) tag_target = self.git_repo.lookup_reference( 'refs/tags/{0}'.format(tag_name)).peel() if tag_target: return Tag(tag_name, tag_target) except KeyError: pass def listall_tags(self): """Returns a list with the names of all tags in this repository. Use lookup_tag to get the Tag object corresponding to eacn name. """ for ref in self.git_repo.listall_references(): if ref.startswith('refs/tags/'): if ref.endswith('^{}'): continue yield ref[10:] # Branch-related methods @property def current_branch(self): if self.git_repo.head_is_detached: b = self.git_repo.lookup_reference('GL_FUSE_ORIG_HEAD').resolve() else: b = self.git_repo.head return self.lookup_branch(b.shorthand) def create_branch(self, name, head): try: return Branch( self.git_repo.create_branch(name, head, False), # force=False self) except ValueError as e: # Edit pygit2's error msg (the message exposes Git details that will # confuse the Gitless's user) raise ValueError( str(e).replace('refs/heads/', '').replace('reference', 'branch')) def lookup_branch(self, branch_name): git_branch = self.git_repo.lookup_branch( branch_name, pygit2.GIT_BRANCH_LOCAL) if git_branch: return Branch(git_branch, self) def listall_branches(self): """Return a list with the names of all the branches in this repository. Use lookup_branch if you want to get the Branch object corresponding to each name. """ return self.git_repo.listall_branches(pygit2.GIT_BRANCH_LOCAL) def switch_current_branch(self, dst_b, move_over=False): """Switches to the given branch. Args: dst_b: the destination branch. move_over: if True, then uncommitted changes made in the current branch are moved to the destination branch (defaults to False). """ if dst_b.is_current: raise ValueError( 'You are already on branch {0}. No need to switch.'.format( dst_b.branch_name)) INFO_SEP = '|' ANCESTOR = 'ancestor' THEIRS = 'theirs' OURS = 'ours' REF_INFO = 'ref_info' CONF_INFO = 'conf_info' MSG_INFO = 'msg_info' git_repo = self.git_repo au_fp = lambda b: os.path.join( self.path, 'GL_AU_{0}'.format(b.branch_name.replace('/', '_'))) update_index = git.bake('update-index', _cwd=self.root) def save(b): msg = _stash_msg(b.branch_name) # Save assumed unchanged info au_fps = ' '.join(b._au_files()) if au_fps: with io.open(au_fp(b), mode='w', encoding=ENCODING) as f: f.write(au_fps) update_index('--no-assume-unchanged', au_fps) if b.merge_in_progress or b.fuse_in_progress: body = {} if move_over: raise GlError( 'Changes can\'t be moved over with a fuse or merge in progress') # Save msg info merge_msg_fp = os.path.join(self.path, 'MERGE_MSG') with io.open(merge_msg_fp, 'r', encoding=ENCODING) as f: merge_msg = f.read() os.remove(merge_msg_fp) body[MSG_INFO] = merge_msg # Save conflict info conf_info = {} index = git_repo.index index.read() if index.conflicts: extract = lambda e: {'mode': e.mode, 'id': str(e.id), 'path': e.path} for ancestor, ours, theirs in index.conflicts: if ancestor: path = ancestor.path ancestor = extract(ancestor) if theirs: path = theirs.path theirs = extract(theirs) if ours: path = ours.path ours = extract(ours) conf_info[path] = {ANCESTOR: ancestor, THEIRS: theirs, OURS: ours} index.add(path) index.write() body[CONF_INFO] = conf_info # Save ref info if b.merge_in_progress: ref_info = {'MERGE_HEAD': str(self._ref_target('MERGE_HEAD'))} self._ref_rm('MERGE_HEAD') else: ref_info = { 'HEAD': str(git_repo.head.target), 'GL_FUSE_ORIG_HEAD': str(self._ref_target('GL_FUSE_ORIG_HEAD')), 'CHERRY_PICK_HEAD': str(self._ref_target('CHERRY_PICK_HEAD')) } self._ref_rm('GL_FUSE_ORIG_HEAD') self._ref_rm('CHERRY_PICK_HEAD') body[REF_INFO] = ref_info msg += INFO_SEP + json.dumps(body) if not move_over: # Stash git.stash.save('--all', '--', msg) def restore(b): s_id, msg = _stash(_stash_msg(b.branch_name)) if not s_id: return def restore_au_info(): au = au_fp(b) if os.path.exists(au): with io.open(au, mode='r', encoding=ENCODING) as f: au_fps = f.read() update_index('--assume-unchanged', au_fps) os.remove(au) split_msg = msg.split(INFO_SEP) if len(split_msg) == 1: # No op to restore # Pop git.stash.pop(s_id) # Restore assumed unchanged info restore_au_info() else: # Restore op body = json.loads(split_msg[1]) # Restore ref info ref_info = body[REF_INFO] if 'GL_FUSE_ORIG_HEAD' in ref_info: # fuse head = git_repo[ref_info['HEAD']] git_repo.set_head(head.id) git_repo.reset(head.id, pygit2.GIT_RESET_HARD) self._ref_create('CHERRY_PICK_HEAD', ref_info['CHERRY_PICK_HEAD']) self._ref_create('GL_FUSE_ORIG_HEAD', ref_info['GL_FUSE_ORIG_HEAD']) else: # merge self._ref_create('MERGE_HEAD', ref_info['MERGE_HEAD']) # Pop git.stash.pop(s_id) # Restore conflict info conf_info = body[CONF_INFO] rm_sentinel = lambda path: '0 {0}\t{1}'.format('0' * 40, path) build_entry = ( lambda e, num: '{mode:o} {id} {0}\t{path}'.format(num, **e)) index_info = [] for path, index_e in conf_info.items(): index_info.append(rm_sentinel(path)) if index_e[ANCESTOR]: index_info.append(build_entry(index_e[ANCESTOR], 1)) if index_e[OURS]: index_info.append(build_entry(index_e[OURS], 2)) if index_e[THEIRS]: index_info.append(build_entry(index_e[THEIRS], 3)) update_index('--unresolve', _in=' '.join(conf_info.keys())) update_index('--index-info', _in='\n'.join(index_info)) # Restore msg info merge_msg_fp = os.path.join(self.path, 'MERGE_MSG') with io.open(merge_msg_fp, 'w', encoding=ENCODING) as f: f.write(body[MSG_INFO]) # Restore assumed unchanged info restore_au_info() save(self.current_branch) git_repo.checkout(dst_b.git_branch) restore(dst_b) class RemoteCollection(object): def __init__(self, git_remote_collection, gl_repo): self.git_remote_collection = git_remote_collection self.gl_repo = gl_repo def __len__(self): return len(self.git_remote_collection) def __iter__(self): return (Remote(r, self.gl_repo) for r in self.git_remote_collection) def __getitem__(self, name): return Remote(self.git_remote_collection.__getitem__(name), self.gl_repo) def __contains__(self, name): try: self.git_remote_collection.__getitem__(name) return True except KeyError: return False def create(self, name, url): if '/' in name: raise ValueError( 'Invalid remote name {0}: remotes can\'t have \'/\''.format(name)) if not url.strip(): raise ValueError('Invalid url {0}'.format(url)) # Check that the given url corresponds to a git repo try: git('ls-remote', '--heads', url) except ErrorReturnCode as e: raise ValueError(stderr(e)) self.git_remote_collection.create(name, url) def delete(self, name): self.git_remote_collection.delete(name) class Remote(object): """Tracked remote repository. Attributes: name: the name of this remote. url: the url of this remote. """ def __init__(self, git_remote, gl_repo): self.git_remote = git_remote self.gl_repo = gl_repo self.name = self.git_remote.name self.url = self.git_remote.url # Branch-related methods def create_branch(self, name, head): if self.lookup_branch(name): raise GlError( 'Branch {0} already exists in remote repository {1}'.format( name, self.name)) # Push won't let us push the creation of a new branch from a SHA. So we # create a temporary local ref, make it point to the commit, and do the # push tmp_b = self.gl_repo.create_branch('gl_tmp_ref', head) try: git.push(self.name, '{0}:{1}'.format(tmp_b, name)) return self.lookup_branch(name) except ErrorReturnCode as e: raise GlError(stderr(e)) finally: tmp_b.delete() def listall_branches(self): """Return a list with the names of all the branches in this repository. Use lookup_branch if you want to get the RemoteBranch object corresponding to each name. """ regex = re.compile(r'.*\trefs/heads/(.*)') for head in stdout(git('ls-remote', '--heads', self.name)).splitlines(): yield regex.match(head).group(1) def lookup_branch(self, branch_name): if not stdout(git('ls-remote', '--heads', self.name, branch_name)): return None # The branch exists in the remote git.fetch(self.git_remote.name, branch_name) git_branch = self.gl_repo.git_repo.lookup_branch( self.git_remote.name + '/' + branch_name, pygit2.GIT_BRANCH_REMOTE) return RemoteBranch(git_branch, self.gl_repo) # Tag-related methods def create_tag(self, name, commit): if self.lookup_tag(name): raise GlError( 'Tag {0} already exists in remote repository {1}'.format( name, self.name)) # We can't create a tag in a remote without creating a local one first. So # we create a temporary local ref, make it point to the commit, and do the # push tmp_t = self.gl_repo.create_tag('gl_tmp_ref', commit) try: git.push(self.name, 'refs/tags/{0}:refs/tags/{1}'.format(tmp_t, name)) return self.lookup_tag(name) except ErrorReturnCode as e: raise GlError(stderr(e)) finally: tmp_t.delete() def listall_tags(self): """Return a list with the names of all tags in this repository. Use lookup_tag if you want to get the RemoteTag object corresponding to each name. """ regex = re.compile(r'.*\trefs/tags/(.*)') for head in stdout(git('ls-remote', '--tags', self.name)).splitlines(): tag_name = regex.match(head).group(1) if tag_name.endswith('^{}'): continue yield tag_name def lookup_tag(self, tag_name): tag_info = stdout(git('ls-remote', '--tags', self.name, tag_name)) if not tag_info: return None # The tag exists in the remote git.fetch(self.git_remote.name, tag_name) regex = re.compile(r'(.*)\trefs/tags/.*') commit_id = regex.match(tag_info).group(1) commit = self.gl_repo.git_repo.get(commit_id).peel(pygit2.GIT_OBJ_COMMIT) return RemoteTag(self.git_remote.name, tag_name, commit) class RemoteTag(object): """A tag that lives on some remote repository. Attributes: tag_name: the name of this tag: remote_name: the name of the remote that represents the remote repository where this tags lives. commit: the commit this tag labels. """ def __init__(self, remote_name, tag_name, commit): self.remote_name = remote_name self.tag_name = tag_name self.commit = commit def delete(self): try: git.push(self.remote_name, ':{0}'.format(self.tag_name)) except ErrorReturnCode as e: raise GlError(stderr(e)) def __str__(self): return self.remote_name + '/' + self.tag_name class RemoteBranch(object): """A branch that lives on some remote repository. Attributes: branch_name: the name of this branch. remote_name: the name of the remote that represents the remote repository where this branch lives. head: commit that is the head of this branch. """ def __init__(self, git_branch, gl_repo): self.git_branch = git_branch self.gl_repo = gl_repo self.remote_name = self.git_branch.remote_name self.branch_name = self.git_branch.branch_name[len(self.remote_name) + 1:] def delete(self): try: git.push(self.remote_name, ':{0}'.format(self.branch_name)) except ErrorReturnCode as e: raise GlError(stderr(e)) @property def target(self): """Object Id of the commit this branch points to.""" self._update() return self.git_branch.target @property def head(self): self._update() return self.git_branch.peel() def history(self, reverse=False): return walker(self.gl_repo.git_repo, self.target, reverse=reverse) def _update(self): git.fetch(self.remote_name, self.branch_name) self.git_branch = self.gl_repo.git_repo.lookup_branch( self.remote_name + '/' + self.branch_name, pygit2.GIT_BRANCH_REMOTE) def __str__(self): return self.remote_name + '/' + self.branch_name class Branch(object): """An independent line of development. Attributes: branch_name: the name of this branch. upstream: the upstream of this branch. is_current: True if this branch is the current branch in the repository it belongs to. merge_in_progress: True if a merge op is in progress on this branch. fuse_in_progress: True if a fuse op is in progress on this branch. head: commit that is the head of this branch. """ def __init__(self, git_branch, gl_repo): self.git_branch = git_branch self.gl_repo = gl_repo self.branch_name = self.git_branch.branch_name def delete(self): if self.is_current: raise BranchIsCurrentError('Can\'t delete the current branch') self.git_branch.delete() # We also cleanup any stash left s_id, _ = _stash(_stash_msg(self.branch_name)) if s_id: git.stash.drop(s_id) @property def upstream(self): git_upstream = self.git_branch.upstream if not git_upstream: return None try: git_upstream.remote_name return RemoteBranch(git_upstream, self.gl_repo) except ValueError: # Upstream is a local branch return Branch(git_upstream, self.gl_repo) @upstream.setter def upstream(self, new_upstream): self.git_branch.upstream = new_upstream.git_branch if new_upstream else None @property def head(self): self._update() return self.git_branch.peel() @head.setter def head(self, new_head): self.gl_repo.git_repo.reset(new_head, pygit2.GIT_RESET_SOFT) @property def target(self): """Object Id of the commit this branch points to.""" self._update() return self.git_branch.target @property def is_current(self): return self.gl_repo.current_branch.branch_name == self.branch_name def _update(self): self.git_branch = self.gl_repo.git_repo.lookup_branch( self.branch_name, pygit2.GIT_BRANCH_LOCAL) def history(self, reverse=False): return walker(self.gl_repo.git_repo, self.target, reverse=reverse) def diff_commits(self, c1, c2): return c1.tree.diff_to_tree(c2.tree) def __str__(self): return self.branch_name @property def _index(self): """Convenience wrapper of Git's index.""" class Index(object): def __init__(self, git_index): self._git_index = git_index self._git_index.read() def __enter__(self): return self def __exit__(self, type, value, traceback): if not value: # no exception self._git_index.write() return True def __getattr__(self, name): return getattr(self._git_index, name) return Index(self.gl_repo.git_repo.index) _st_map = { # git status: gl status, exists_at_head, exists_in_wd, modified, conflict pygit2.GIT_STATUS_CURRENT: (GL_STATUS_TRACKED, True, True, False, False), pygit2.GIT_STATUS_IGNORED: (GL_STATUS_IGNORED, False, True, True, False), pygit2.GIT_STATUS_CONFLICTED: (GL_STATUS_TRACKED, True, True, True, True), ### WT_* ### pygit2.GIT_STATUS_WT_NEW: (GL_STATUS_UNTRACKED, False, True, True, False), pygit2.GIT_STATUS_WT_MODIFIED: (GL_STATUS_TRACKED, True, True, True, False), pygit2.GIT_STATUS_WT_DELETED: (GL_STATUS_TRACKED, True, False, True, False), ### INDEX_* ### pygit2.GIT_STATUS_INDEX_NEW: (GL_STATUS_TRACKED, False, True, True, False), pygit2.GIT_STATUS_INDEX_MODIFIED: ( GL_STATUS_TRACKED, True, True, True, False), pygit2.GIT_STATUS_INDEX_DELETED: ( GL_STATUS_TRACKED, True, False, True, False), ### WT_NEW | INDEX_* ### # WT_NEW | INDEX_NEW -> can't happen # WT_NEW | INDEX_MODIFIED -> can't happen # WT_NEW | INDEX_DELETED -> could happen if user broke gl layer (e.g., did # `git rm` and then created file with same name). pygit2.GIT_STATUS_WT_NEW | pygit2.GIT_STATUS_INDEX_DELETED: ( GL_STATUS_TRACKED, True, True, True, False), ### WT_MODIFIED | INDEX_* ### pygit2.GIT_STATUS_WT_MODIFIED | pygit2.GIT_STATUS_INDEX_NEW: ( GL_STATUS_TRACKED, False, True, True, False), pygit2.GIT_STATUS_WT_MODIFIED | pygit2.GIT_STATUS_INDEX_MODIFIED: ( GL_STATUS_TRACKED, True, True, True, False), # WT_MODIFIED | INDEX_DELETED -> can't happen ### WT_DELETED | INDEX_* ### -> can't happen } FileStatus = collections.namedtuple( 'FileStatus', [ 'fp', 'type', 'exists_at_head', 'exists_in_wd', 'modified', 'in_conflict']) def _au_files(self): for f_out in stdout( git('ls-files', '-v', _cwd=self.gl_repo.root)).splitlines(): if f_out[0] == 'h': yield f_out[2:].strip() def status(self): """Return a generator of file statuses (see FileStatus). Ignored and tracked unmodified files are not reported. File paths are always relative to the repo root. """ for fp, git_s in self.gl_repo.git_repo.status().items(): yield self.FileStatus(fp, *self._st_map[git_s]) # status doesn't report au files au_files = self._au_files() if au_files: for fp in au_files: exists_in_wd = os.path.exists(os.path.join(self.gl_repo.root, fp)) yield self.FileStatus( fp, GL_STATUS_UNTRACKED, True, exists_in_wd, True, False) def status_file(self, path): """Return the status (see FileStatus) of the given path.""" return self._status_file(path)[0] def _status_file(self, path): _check_path_is_repo_relative(path) git_st = self.gl_repo.git_repo.status_file(_get_git_path(path)) root = self.gl_repo.root cmd_out = stdout(git('ls-files', '-v', '--full-name', path, _cwd=root)) is_au = cmd_out and cmd_out[0] == 'h' if is_au: exists_in_wd = os.path.exists(os.path.join(root, path)) f_st = self.FileStatus( path, GL_STATUS_UNTRACKED, True, exists_in_wd, True, False) else: f_st = self.FileStatus(path, *self._st_map[git_st]) return f_st, git_st, is_au def path_is_ignored(self, path): _check_path_is_repo_relative(path) git_path = _get_git_path(path) return self.gl_repo.git_repo.path_is_ignored(git_path) # File-related methods def track_file(self, path): """Start tracking changes to path.""" _check_path_is_repo_relative(path) gl_st, git_st, is_au = self._status_file(path) if gl_st.type == GL_STATUS_TRACKED: raise ValueError('File {0} is already tracked'.format(path)) elif gl_st.type == GL_STATUS_IGNORED: raise ValueError( 'File {0} is ignored. Edit the .gitignore file to stop ignoring ' 'file {0}'.format(path)) # If we reached this point we know that the file to track is a untracked # file. This means that in the Git world, the file could be either: # (i) a new file for Git => add the file; # (ii) an assumed unchanged file => unmark it. if git_st == pygit2.GIT_STATUS_WT_NEW: # Case (i) with self._index as index: git_path = _get_git_path(path) index.add(git_path) elif is_au: # Case (ii) git('update-index', '--no-assume-unchanged', path, _cwd=self.gl_repo.root) else: raise GlError('File {0} in unknown status {1}'.format(path, git_st)) def untrack_file(self, path): """Stop tracking changes to path.""" _check_path_is_repo_relative(path) gl_st, git_st, is_au = self._status_file(path) if gl_st.type == GL_STATUS_UNTRACKED: raise ValueError('File {0} is already untracked'.format(path)) elif gl_st.type == GL_STATUS_IGNORED: raise ValueError( 'File {0} is ignored. Edit the .gitignore file to stop ignoring ' 'file {0}'.format(path)) elif gl_st.in_conflict: raise ValueError('File {0} has conflicts'.format(path)) # If we reached this point we know that the file to untrack is a tracked # file. This means that in the Git world, the file could be either: # (i) a new file for Git that is staged (the user executed `gl track` on # an uncommitted file) => reset changes; # (ii) the file is a previously committed file => mark it as assumed # unchanged. if git_st == pygit2.GIT_STATUS_INDEX_NEW: # Case (i) with self._index as index: git_path = _get_git_path(path) index.remove(git_path) elif not is_au: # Case (ii) git('update-index', '--assume-unchanged', path, _cwd=self.gl_repo.root) else: raise GlError('File {0} in unknown status {1}'.format(path, git_st)) def resolve_file(self, path): """Mark the given path as resolved.""" _check_path_is_repo_relative(path) gl_st, _, _ = self._status_file(path) if not gl_st.in_conflict: raise ValueError('File {0} has no conflicts'.format(path)) with self._index as index: git_path = _get_git_path(path) index.add(git_path) def checkout_file(self, path, commit): """Checkouts the given path at the given commit.""" _check_path_is_repo_relative(path) git_path = _get_git_path(path) o = self.gl_repo.git_repo[commit.tree[git_path].id] assert o.type != pygit2.GIT_OBJ_COMMIT assert o.type != pygit2.GIT_OBJ_TAG if o.type == pygit2.GIT_OBJ_BLOB: full_path = os.path.join(self.gl_repo.root, path) dirname = os.path.dirname(full_path) if not os.path.exists(dirname): try: os.makedirs(dirname) except OSError as exc: # guard against race condition if exc.errno != errno.EEXIST: raise with io.open(full_path, mode='wb') as dst: dst.write(o.data) # So as to not get confused with the status of the file we also add it. # This prevents getting into a situation in which the staged version is # different from the working version. In such a case, the file would # appear as modified to Gitless when it shouldn't. This is also consistent # with the behavior of `git checkout ` that also adds the # file to the staging area. with self._index as index: index.add(git_path) elif o.type == pygit2.GIT_OBJ_TREE: raise PathIsDirectoryError( 'Path {0} at {1} is a directory and not a file'.format( path, commit.id)) else: raise Exception('Unexpected object type {0}'.format(o.type)) def get_paths(self, path, commit): """Return a generator of all filepaths under path at commit.""" _check_path_is_repo_relative(path) git_path = _get_git_path(path) tree = self.gl_repo.git_repo[commit.tree[git_path].id] assert tree.type == pygit2.GIT_OBJ_TREE for tree_entry in tree: tree_entry_path = os.path.join(path, tree_entry.name) if tree_entry.type == 'tree': for fp in self.get_paths(tree_entry_path, commit): yield fp else: yield tree_entry_path def diff_file(self, path): """Diff the working version of path with its committed version.""" _check_path_is_repo_relative(path) git_repo = self.gl_repo.git_repo git_path = _get_git_path(path) try: blob_at_head = git_repo[git_repo.head.peel().tree[git_path].id] except KeyError: # no blob at head wt_blob = git_repo[git_repo.create_blob_fromworkdir(git_path)] nil_blob = git_repo[git_repo.create_blob('')] return nil_blob.diff(wt_blob, 0, git_path, git_path) try: wt_blob = git_repo[git_repo.create_blob_fromworkdir(git_path)] except KeyError: # no blob at wd (the file was deleted) nil_blob = git_repo[git_repo.create_blob('')] return blob_at_head.diff(nil_blob, 0, git_path, git_path) return blob_at_head.diff(wt_blob, 0, git_path, git_path) # Merge-related methods def merge(self, src, op_cb=None): """Merges the divergent changes of the src branch onto this one.""" self._check_is_current() self._check_op_not_in_progress() result, unused_ff_conf = self.gl_repo.git_repo.merge_analysis(src.target) if result & pygit2.GIT_MERGE_ANALYSIS_UP_TO_DATE: raise GlError('No commits to merge') try: git.merge(src, '--no-ff') except ErrorReturnCode as e: err = stderr(e) if not 'stash' in err: raise GlError(stdout(e) + err) if op_cb and op_cb.save: op_cb.save() git.stash.save('--', _stash_msg_merge(self)) try: git.merge(src, '--no-ff') except ErrorReturnCode as e: raise GlError(stdout(e) + stderr(e)) self._state_cleanup() restore_fn = op_cb.restore_ok if op_cb else None self._safe_restore(_stash_msg_merge, restore_fn=restore_fn) def merge_continue(self, op_cb=None): if not self.merge_in_progress: raise GlError('No merge in progress, nothing to continue') self._state_cleanup() restore_fn = op_cb.restore_ok if op_cb else None self._safe_restore(_stash_msg_merge, restore_fn=restore_fn) @property def merge_in_progress(self): return self.gl_repo._ref_exists('MERGE_HEAD') def abort_merge(self): if not self.merge_in_progress: raise GlError('No merge in progress, nothing to abort') git.merge(abort=True) # Fuse-related methods @property def _fuse_commits_fp(self): return self.gl_repo._fuse_commits_fp(self) def _save_fuse_commits(self, commits): path = self._fuse_commits_fp using_tmp = False if os.path.exists(path): path = path + '_tmp' using_tmp = True with io.open(path, mode='w', encoding=ENCODING) as f: for ci in commits: f.write(ci.id.hex + '\n') if using_tmp: shutil.move(path, self._fuse_commits_fp) def _load_fuse_commits(self): git_repo = self.gl_repo.git_repo with io.open(self._fuse_commits_fp, mode='r', encoding=ENCODING) as f: for ci_id in f: ci_id = ci_id.strip() yield git_repo[ci_id] os.remove(self._fuse_commits_fp) def fuse(self, src, ip, only=None, exclude=None, op_cb=None): """Fuse the given commits onto this branch. Args: src: the branch (Branch obj) to fuse commits from. ip: id of the commit to act as the insertion point. The commits to fuse are inserted after this commit. ip has to correspond to one of the divergent commits from self or the divergent point. only: ids of commits to use only. exclude: ids of commtis to exclude. op_cb: see OpCb. """ self._check_is_current() self._check_op_not_in_progress() save_fn = op_cb.save if op_cb else None repo = self.gl_repo ip_to_src = src.history(reverse=True) ip_to_src.hide(ip) divergent_commits, fuse_commits = itertools.tee(ip_to_src, 2) if only: fuse_commits = (ci for ci in fuse_commits if ci.id in only) elif exclude: fuse_commits = (ci for ci in fuse_commits if ci.id not in exclude) fuse_commits, _fuse_commits = itertools.tee(fuse_commits, 2) if not any(_fuse_commits): raise GlError('No commits to fuse') # Figure out where to detach head # If the ip is not the mb, then we need to detach at the ip, because the # first div commit won't have the ip as its parent # But, if the ip **is** the merge base we can advance the head until the div # commits and the commits to fuse diverge detach_point = ip if ip == repo.merge_base(self, src): for ci, fuse_ci in zip(divergent_commits, fuse_commits): if ci.id != fuse_ci.id: fuse_commits = itertools.chain([fuse_ci], fuse_commits) break detach_point = ci.id if op_cb and op_cb.apply_ok: op_cb.apply_ok(ci) after_commits = self.history(reverse=True) after_commits.hide(ip) commits = itertools.chain(fuse_commits, after_commits) commits, _commits = itertools.tee(commits, 2) if not any(_commits): # it's a ff self._safe_reset(detach_point, _stash_msg_fuse, save_fn=save_fn) restore_fn = op_cb.restore_ok if op_cb else None self._safe_restore(_stash_msg_fuse, restore_fn=restore_fn) return # We are going to have to do some cherry-picking # Save the current head so that we remember the current branch head_fp = os.path.join(repo.path, 'HEAD') orig_head_fp = os.path.join(repo.path, 'GL_FUSE_ORIG_HEAD') shutil.copyfile(head_fp, orig_head_fp) # Detach head so that reset doesn't reset master and instead # resets the head ref repo.git_repo.set_head(repo.git_repo.head.peel().id) self._safe_reset(detach_point, _stash_msg_fuse, save_fn=save_fn) self._fuse(commits, op_cb=op_cb) def fuse_continue(self, op_cb=None): if not self.fuse_in_progress: raise GlError('No fuse in progress, nothing to continue') commits = self._load_fuse_commits() self._fuse(commits, op_cb=op_cb) def _fuse(self, commits, op_cb=None): git_repo = self.gl_repo.git_repo committer = git_repo.default_signature for ci in commits: git_repo.cherrypick(ci.id) index = self._index if index.conflicts: if op_cb and op_cb.apply_err: op_cb.apply_err(ci) self._save_fuse_commits(commits) raise GlError('There are conflicts you need to resolve') if op_cb and op_cb.apply_ok: op_cb.apply_ok(ci) tree_oid = index.write_tree(git_repo) git_repo.create_commit( 'HEAD', # the name of the reference to update ci.author, committer, ci.message, tree_oid, [git_repo.head.target]) # We are done fusing => update original branch and re-attach head orig_branch_ref = git_repo.lookup_reference('GL_FUSE_ORIG_HEAD').resolve() orig_branch_ref.set_target(git_repo.head.target) git_repo.set_head(orig_branch_ref.name) self._state_cleanup() restore_fn = op_cb.restore_ok if op_cb else None self._safe_restore(_stash_msg_fuse, restore_fn=restore_fn) @property def fuse_in_progress(self): return self.gl_repo._ref_exists('GL_FUSE_ORIG_HEAD') def abort_fuse(self, op_cb=None): if not self.fuse_in_progress: raise GlError('No fuse in progress, nothing to abort') git_repo = self.gl_repo.git_repo git_repo.set_head(git_repo.lookup_reference('GL_FUSE_ORIG_HEAD').target) git_repo.reset(git_repo.head.peel().hex, pygit2.GIT_RESET_HARD) self._state_cleanup() restore_fn = op_cb.restore_ok if op_cb else None self._safe_restore(_stash_msg_fuse, restore_fn=restore_fn) def _state_cleanup(self): self.gl_repo.git_repo.state_cleanup() if os.path.exists(self._fuse_commits_fp): os.remove(self._fuse_commits_fp) self.gl_repo._ref_rm('GL_FUSE_ORIG_HEAD') def _safe_reset(self, cid, msg_fn, save_fn=None): git_repo = self.gl_repo.git_repo tree = git_repo[cid].tree try: git_repo.checkout_tree(tree) except pygit2.GitError: # conflicts prevent checkout # TODO: this hack will cover most cases, but it won't help if the conflict # is caused by untracked files (nonetheless `stash pop` won't work in that # case either so we need to find an alternative way of doing this) if save_fn: save_fn() git.stash.save('--', msg_fn(self)) git_repo.checkout_tree(tree) git_repo.reset(cid, pygit2.GIT_RESET_SOFT) def _safe_restore(self, msg_fn, restore_fn=None): s_id, _ = _stash(msg_fn(self)) if s_id: try: git.stash.pop(s_id) if restore_fn: restore_fn() except ErrorReturnCode: raise ApplyFailedError( 'Uncommitted changes failed to apply onto the new head of the ' 'branch') def create_commit(self, files, msg, author=None, partials=None): """Record a new commit on this branch. Args: files: the (modified) files to commit. msg: the commit message. author: the author of the commit (defaults to the default author according to the repository's configuration). partials: list of files to commit partially. """ git_repo = self.gl_repo.git_repo if not author: author = git_repo.default_signature index = self._index if index.conflicts: raise GlError('Unresolved conflicts') # If file f is in the list of files to be committed => commit the working # version (or the staged version if f is in the list of partially committed # files) and clear the staged version. # If file f is not in the list of files to be committed => leave its staged # version (if any) intact. if partials is None: partials = [] def get_tree_and_update_index(): def update(): """Add/remove files to the index.""" for f in files: assert not os.path.isabs(f) git_f = _get_git_path(f) if not os.path.exists(os.path.join(self.gl_repo.root, f)): index.remove(git_f) elif f not in partials: index.add(git_f) # Update index to how it should look like after the commit partial_entries = {} with index: update() for f in partials: git_f = _get_git_path(f) partial_entries[f] = index._git_index[git_f] # To create the commit tree with only the changes to the given files we: # (i) reset the index to HEAD, # (ii) update it with the changes to commit, # (iii) create a tree out of this modified index, and # (iv) discard the changes after being done. index.read_tree(git_repo.head.peel().tree) update() for f in partial_entries.keys(): index.add(partial_entries[f]) tree_oid = index.write_tree() index.read() # discard changes return tree_oid parents = [git_repo.head.target] if self.merge_in_progress: parents.append(git_repo.lookup_reference('MERGE_HEAD').target) ci_oid = git_repo.create_commit( 'HEAD', # will point to the new commit author, author, # use author as committer msg, get_tree_and_update_index(), # the commit tree parents) return self.gl_repo.git_repo[ci_oid] def publish(self, branch): self._check_op_not_in_progress() if not isinstance(branch, RemoteBranch): # TODO: allow this raise GlError( 'Can\'t publish to a local branch (yet---this will be implemented in ' 'the future)') try: assert self.branch_name.strip() assert branch.branch_name in self.gl_repo.remotes[ branch.remote_name].listall_branches() cmd = git.push( branch.remote_name, '{0}:{1}'.format(self.branch_name, branch.branch_name)) if 'Everything up-to-date' in stderr(cmd): raise GlError('No commits to publish') except ErrorReturnCode as e: err_msg = stderr(e) if 'Updates were rejected' in err_msg: raise GlError('There are changes you need to fuse/merge') raise GlError(err_msg) # Branch helpers def _check_op_not_in_progress(self): if self.merge_in_progress: raise GlError('Merge in progress') if self.fuse_in_progress: raise GlError('Fuse in progress') def _check_is_current(self): if not self.is_current: raise BranchIsCurrentError( 'Branch {0} is the current branch'.format(self.branch_name)) class Tag(object): """Static label for a commit. Attributes: tag_name: the name of this tag. commit: the commit this tag labels. """ def __init__(self, tag_name, commit): self.tag_name = tag_name self.commit = commit def delete(self): git.tag('-d', self.tag_name) def __str__(self): return self.tag_name # Helpers for stashing def _stash(pattern): """Returns the id and msg of the stash that matches the given pattern.""" out = stdout(git.stash.list(grep=pattern, format='|*|%gd|*|%B|*|')) if not out: return None, None result = re.match(r'\|\*\|(stash@\{.+\})\|\*\|(.*)\|\*\|', out, re.DOTALL) if not result: raise GlError('Unexpected output of git stash: {0}'.format(out)) return result.group(1).strip(), result.group(2).strip() def _stash_msg(name): return '---gl-{0}---'.format(name) def _stash_msg_fuse(name): return _stash_msg('fuse-{0}'.format(name)) def _stash_msg_merge(name): return _stash_msg('merge-{0}'.format(name)) # Misc OpCb = collections.namedtuple( 'OpCb', ['apply_ok', 'apply_err', 'save', 'restore_ok']) def stdout(p): try: pstdout = p.stdout.decode(ENCODING) except AttributeError: pstdout = p.stdout return pstdout def stderr(p): try: pstderr = p.stderr.decode(ENCODING) except AttributeError: pstderr = p.stderr return pstderr def walker(git_repo, target, reverse): flags = pygit2.GIT_SORT_TOPOLOGICAL | pygit2.GIT_SORT_TIME if reverse: flags = flags | pygit2.GIT_SORT_REVERSE return git_repo.walk(target, flags) def _get_git_path(path): return path if sys.platform != 'win32' else path.replace('\\', '/') def _check_path_is_repo_relative(path): if os.path.isabs(path): raise ValueError( "path {0} is absolute but should be relative to the repo root".format(path)) gitless-0.8.8/gitless/tests/000077500000000000000000000000001347331605400160235ustar00rootroot00000000000000gitless-0.8.8/gitless/tests/__init__.py000066400000000000000000000000001347331605400201220ustar00rootroot00000000000000gitless-0.8.8/gitless/tests/test_core.py000066400000000000000000001050641347331605400203720ustar00rootroot00000000000000# -*- coding: utf-8 -*- # Gitless - a version control system built on top of Git # Licensed under MIT """Core unit tests.""" from __future__ import unicode_literals from functools import wraps import os import shutil import tempfile import sys if sys.platform != 'win32': from sh import git else: from pbs import Command git = Command('git') from gitless import core import gitless.tests.utils as utils_lib TRACKED_FP = 'f1' TRACKED_FP_CONTENTS_1 = 'f1-1\n' TRACKED_FP_CONTENTS_2 = 'f1-2\n' TRACKED_FP_WITH_SPACE = 'f1 space' UNTRACKED_FP = 'f2' UNTRACKED_FP_CONTENTS = 'f2' UNTRACKED_FP_WITH_SPACE = 'f2 space' IGNORED_FP = 'f3' IGNORED_FP_WITH_SPACE = 'f3 space' NONEXISTENT_FP = 'nonexistent' NONEXISTENT_FP_WITH_SPACE = 'nonexistent space' DIR = 'dir' UNTRACKED_DIR_FP = os.path.join(DIR, 'f1') UNTRACKED_DIR_FP_WITH_SPACE = os.path.join(DIR, 'f1 space') TRACKED_DIR_FP = os.path.join(DIR, 'f2') TRACKED_DIR_FP_WITH_SPACE = os.path.join(DIR, 'f2 space') DIR_DIR = os.path.join(DIR, DIR) UNTRACKED_DIR_DIR_FP = os.path.join(DIR_DIR, 'f1') UNTRACKED_DIR_DIR_FP_WITH_SPACE = os.path.join(DIR_DIR, 'f1 space') TRACKED_DIR_DIR_FP = os.path.join(DIR_DIR, 'f2') TRACKED_DIR_DIR_FP_WITH_SPACE = os.path.join(DIR_DIR, 'f2 space') ALL_FPS_IN_WD = [ TRACKED_FP, TRACKED_FP_WITH_SPACE, UNTRACKED_FP, UNTRACKED_FP_WITH_SPACE, IGNORED_FP, IGNORED_FP_WITH_SPACE, UNTRACKED_DIR_FP, UNTRACKED_DIR_FP_WITH_SPACE, TRACKED_DIR_FP, TRACKED_DIR_FP_WITH_SPACE, UNTRACKED_DIR_DIR_FP, UNTRACKED_DIR_DIR_FP_WITH_SPACE, TRACKED_DIR_DIR_FP, TRACKED_DIR_DIR_FP_WITH_SPACE, '.gitignore'] ALL_DIR_FPS_IN_WD = [ TRACKED_DIR_FP, TRACKED_DIR_FP_WITH_SPACE, UNTRACKED_DIR_FP, UNTRACKED_DIR_FP_WITH_SPACE, TRACKED_DIR_DIR_FP, TRACKED_DIR_DIR_FP_WITH_SPACE, UNTRACKED_DIR_DIR_FP, UNTRACKED_DIR_DIR_FP_WITH_SPACE] BRANCH = 'b1' REMOTE_BRANCH = 'rb' FP_IN_CONFLICT = 'f_conflict' DIR_FP_IN_CONFLICT = os.path.join(DIR, FP_IN_CONFLICT) # Helpers class TestCore(utils_lib.TestBase): """Base class for core tests.""" def setUp(self): super(TestCore, self).setUp('gl-core-test') git.init() utils_lib.set_test_config() self.repo = core.Repository() def assert_contents_unchanged(*fps): """Decorator that fails the test if the contents of the file fp changed.""" def prop(*args, **kwargs): return utils_lib.read_file return __assert_decorator('Contents', prop, *fps) def assert_status_unchanged(*fps): """Decorator that fails the test if the status of fp changed.""" def prop(self, *args, **kwargs): return self.curr_b.status_file return __assert_decorator('Status', prop, *fps) def assert_no_side_effects(*fps): """Decorator that fails the test if the contents or status of fp changed.""" def decorator(f): @assert_contents_unchanged(*fps) @assert_status_unchanged(*fps) @wraps(f) def wrapper(*args, **kwargs): f(*args, **kwargs) return wrapper return decorator def __assert_decorator(msg, prop, *fps): def decorator(f): @wraps(f) def wrapper(*args, **kwargs): self = args[0] # We save up the cwd to chdir to it after the test has run so that the # the given fps still "work" even if the test changed the cwd. cwd_before = os.getcwd() before_list = [prop(*args, **kwargs)(fp) for fp in fps] f(*args, **kwargs) os.chdir(cwd_before) after_list = [prop(*args, **kwargs)(fp) for fp in fps] for fp, before, after in zip(fps, before_list, after_list): self.assertEqual( before, after, '{0} of file "{1}" changed: from "{2}" to "{3}"'.format( msg, fp, before, after)) return wrapper return decorator # Unit tests for file related operations class TestFile(TestCore): """Base class for file tests.""" def setUp(self): super(TestFile, self).setUp() # Build up an interesting mock repo utils_lib.write_file(TRACKED_FP, contents=TRACKED_FP_CONTENTS_1) utils_lib.write_file(TRACKED_FP_WITH_SPACE, contents=TRACKED_FP_CONTENTS_1) utils_lib.write_file(TRACKED_DIR_FP, contents=TRACKED_FP_CONTENTS_1) utils_lib.write_file( TRACKED_DIR_FP_WITH_SPACE, contents=TRACKED_FP_CONTENTS_1) utils_lib.write_file(TRACKED_DIR_DIR_FP, contents=TRACKED_FP_CONTENTS_1) utils_lib.write_file( TRACKED_DIR_DIR_FP_WITH_SPACE, contents=TRACKED_FP_CONTENTS_1) git.add( TRACKED_FP, TRACKED_FP_WITH_SPACE, TRACKED_DIR_FP, TRACKED_DIR_FP_WITH_SPACE, TRACKED_DIR_DIR_FP, TRACKED_DIR_DIR_FP_WITH_SPACE) git.commit( TRACKED_FP, TRACKED_FP_WITH_SPACE, TRACKED_DIR_FP, TRACKED_DIR_FP_WITH_SPACE, TRACKED_DIR_DIR_FP, TRACKED_DIR_DIR_FP_WITH_SPACE, m='1') utils_lib.write_file(TRACKED_FP, contents=TRACKED_FP_CONTENTS_2) utils_lib.write_file(TRACKED_FP_WITH_SPACE, contents=TRACKED_FP_CONTENTS_2) utils_lib.write_file(TRACKED_DIR_FP, contents=TRACKED_FP_CONTENTS_2) utils_lib.write_file( TRACKED_DIR_FP_WITH_SPACE, contents=TRACKED_FP_CONTENTS_2) utils_lib.write_file(TRACKED_DIR_DIR_FP, contents=TRACKED_FP_CONTENTS_2) utils_lib.write_file( TRACKED_DIR_DIR_FP_WITH_SPACE, contents=TRACKED_FP_CONTENTS_2) git.commit( TRACKED_FP, TRACKED_FP_WITH_SPACE, TRACKED_DIR_FP, TRACKED_DIR_FP_WITH_SPACE, TRACKED_DIR_DIR_FP, TRACKED_DIR_DIR_FP_WITH_SPACE, m='2') utils_lib.write_file(UNTRACKED_FP) utils_lib.write_file(UNTRACKED_FP_WITH_SPACE) utils_lib.write_file(UNTRACKED_DIR_FP) utils_lib.write_file(UNTRACKED_DIR_FP_WITH_SPACE) utils_lib.write_file(UNTRACKED_DIR_DIR_FP) utils_lib.write_file(UNTRACKED_DIR_DIR_FP_WITH_SPACE) utils_lib.write_file( '.gitignore', contents='{0}\n{1}'.format( IGNORED_FP, IGNORED_FP_WITH_SPACE)) utils_lib.write_file(IGNORED_FP) utils_lib.write_file(IGNORED_FP_WITH_SPACE) self.curr_b = self.repo.current_branch class TestFileTrack(TestFile): def __assert_track_untracked(self, *fps): root = self.repo.root for fp in fps: fp = os.path.relpath(fp, root) self.curr_b.track_file(fp) st = self.curr_b.status_file(fp) self.assertEqual( core.GL_STATUS_TRACKED, st.type, 'Track of fp "{0}" failed: expected status.type={1}, got ' 'status.type={2}'.format(fp, core.GL_STATUS_TRACKED, st.type)) @assert_contents_unchanged( UNTRACKED_FP, UNTRACKED_FP_WITH_SPACE, UNTRACKED_DIR_FP, UNTRACKED_DIR_FP_WITH_SPACE, UNTRACKED_DIR_DIR_FP, UNTRACKED_DIR_DIR_FP_WITH_SPACE) def test_track_untracked(self): self.__assert_track_untracked( UNTRACKED_FP, UNTRACKED_FP_WITH_SPACE, UNTRACKED_DIR_FP, UNTRACKED_DIR_FP_WITH_SPACE, UNTRACKED_DIR_DIR_FP, UNTRACKED_DIR_DIR_FP_WITH_SPACE) @assert_contents_unchanged( UNTRACKED_DIR_FP, UNTRACKED_DIR_FP_WITH_SPACE, UNTRACKED_DIR_DIR_FP, UNTRACKED_DIR_DIR_FP_WITH_SPACE) def test_track_untracked_relative(self): os.chdir(DIR) self.__assert_track_untracked( os.path.relpath(UNTRACKED_DIR_FP, DIR), os.path.relpath(UNTRACKED_DIR_FP_WITH_SPACE, DIR)) os.chdir(DIR) self.__assert_track_untracked( os.path.relpath(UNTRACKED_DIR_DIR_FP, DIR_DIR), os.path.relpath(UNTRACKED_DIR_DIR_FP_WITH_SPACE, DIR_DIR)) def __assert_track_tracked(self, *fps): root = self.repo.root for fp in fps: fp = os.path.relpath(fp, root) self.assertRaisesRegexp( ValueError, 'already tracked', self.curr_b.track_file, fp) @assert_no_side_effects( TRACKED_FP, TRACKED_FP_WITH_SPACE, TRACKED_DIR_FP, TRACKED_DIR_FP_WITH_SPACE, TRACKED_DIR_DIR_FP, TRACKED_DIR_DIR_FP_WITH_SPACE) def test_track_tracked_fp(self): self.__assert_track_tracked( TRACKED_FP, TRACKED_FP_WITH_SPACE, TRACKED_DIR_FP, TRACKED_DIR_FP_WITH_SPACE, TRACKED_DIR_DIR_FP, TRACKED_DIR_DIR_FP_WITH_SPACE) @assert_no_side_effects( TRACKED_DIR_FP, TRACKED_DIR_FP_WITH_SPACE, TRACKED_DIR_DIR_FP, TRACKED_DIR_DIR_FP_WITH_SPACE) def test_track_tracked_relative(self): os.chdir(DIR) self.__assert_track_tracked( os.path.relpath(TRACKED_DIR_FP, DIR), os.path.relpath(TRACKED_DIR_FP_WITH_SPACE, DIR)) os.chdir(DIR) self.__assert_track_tracked( os.path.relpath(TRACKED_DIR_DIR_FP, DIR_DIR), os.path.relpath(TRACKED_DIR_DIR_FP_WITH_SPACE, DIR_DIR)) def __assert_track_nonexistent_fp(self, *fps): root = self.repo.root for fp in fps: fp = os.path.relpath(fp, root) self.assertRaises(KeyError, self.curr_b.track_file, fp) def test_track_nonexistent_fp(self): self.__assert_track_nonexistent_fp( NONEXISTENT_FP, NONEXISTENT_FP_WITH_SPACE) def __assert_track_ignored(self, *fps): root = self.repo.root for fp in fps: fp = os.path.relpath(fp, root) self.assertRaisesRegexp( ValueError, 'is ignored', self.curr_b.track_file, fp) @assert_no_side_effects(IGNORED_FP, IGNORED_FP_WITH_SPACE) def test_track_ignored(self): self.__assert_track_ignored(IGNORED_FP, IGNORED_FP_WITH_SPACE) class TestFileUntrack(TestFile): def __assert_untrack_tracked(self, *fps): root = self.repo.root for fp in fps: fp = os.path.relpath(fp, root) self.curr_b.untrack_file(fp) st = self.curr_b.status_file(fp) self.assertEqual( core.GL_STATUS_UNTRACKED, st.type, 'Untrack of fp "{0}" failed: expected status.type={1}, got ' 'status.type={2}'.format(fp, core.GL_STATUS_UNTRACKED, st.type)) @assert_contents_unchanged( TRACKED_FP, TRACKED_FP_WITH_SPACE, TRACKED_DIR_FP, TRACKED_DIR_FP_WITH_SPACE, TRACKED_DIR_DIR_FP, TRACKED_DIR_DIR_FP_WITH_SPACE) def test_untrack_tracked(self): self.__assert_untrack_tracked( TRACKED_FP, TRACKED_FP_WITH_SPACE, TRACKED_DIR_FP, TRACKED_DIR_FP_WITH_SPACE, TRACKED_DIR_DIR_FP, TRACKED_DIR_DIR_FP_WITH_SPACE) @assert_contents_unchanged( TRACKED_DIR_FP, TRACKED_DIR_FP_WITH_SPACE, TRACKED_DIR_DIR_FP, TRACKED_DIR_DIR_FP_WITH_SPACE) def test_untrack_tracked_relative(self): os.chdir(DIR) self.__assert_untrack_tracked( os.path.relpath(TRACKED_DIR_FP, DIR), os.path.relpath(TRACKED_DIR_FP_WITH_SPACE, DIR)) os.chdir(DIR) self.__assert_untrack_tracked( os.path.relpath(TRACKED_DIR_DIR_FP, DIR_DIR), os.path.relpath(TRACKED_DIR_DIR_FP_WITH_SPACE, DIR_DIR)) def __assert_untrack_error(self, msg, *fps): root = self.repo.root for fp in fps: fp = os.path.relpath(fp, root) self.assertRaisesRegexp(ValueError, msg, self.curr_b.untrack_file, fp) def __assert_untrack_untracked(self, *fps): self.__assert_untrack_error('already untracked', *fps) @assert_no_side_effects( UNTRACKED_FP, UNTRACKED_FP_WITH_SPACE, UNTRACKED_DIR_FP, UNTRACKED_DIR_FP_WITH_SPACE, UNTRACKED_DIR_DIR_FP, UNTRACKED_DIR_DIR_FP_WITH_SPACE) def test_untrack_untracked_fp(self): self.__assert_untrack_untracked( UNTRACKED_FP, UNTRACKED_FP_WITH_SPACE, UNTRACKED_DIR_FP, UNTRACKED_DIR_FP_WITH_SPACE, UNTRACKED_DIR_DIR_FP, UNTRACKED_DIR_DIR_FP_WITH_SPACE) @assert_contents_unchanged( UNTRACKED_DIR_FP, UNTRACKED_DIR_FP_WITH_SPACE, UNTRACKED_DIR_DIR_FP, UNTRACKED_DIR_DIR_FP_WITH_SPACE) def test_untrack_untracked_relative(self): os.chdir(DIR) self.__assert_untrack_untracked( os.path.relpath(UNTRACKED_DIR_FP, DIR), os.path.relpath(UNTRACKED_DIR_FP_WITH_SPACE, DIR)) os.chdir(DIR) self.__assert_untrack_untracked( os.path.relpath(UNTRACKED_DIR_DIR_FP, DIR_DIR), os.path.relpath(UNTRACKED_DIR_DIR_FP_WITH_SPACE, DIR_DIR)) def __assert_untrack_nonexistent_fp(self, *fps): root = self.repo.root for fp in fps: fp = os.path.relpath(fp, root) self.assertRaises(KeyError, self.curr_b.untrack_file, fp) def test_untrack_nonexistent_fp(self): self.__assert_untrack_nonexistent_fp( NONEXISTENT_FP, NONEXISTENT_FP_WITH_SPACE) def __assert_untrack_ignored(self, *fps): self.__assert_untrack_error('is ignored', *fps) @assert_no_side_effects(IGNORED_FP, IGNORED_FP_WITH_SPACE) def test_untrack_ignored(self): self.__assert_untrack_ignored(IGNORED_FP, IGNORED_FP_WITH_SPACE) class TestFileCheckout(TestFile): def __assert_checkout_head(self, *fps): root = self.repo.root for fp in fps: utils_lib.write_file(fp, contents='contents') self.curr_b.checkout_file( os.path.relpath(fp, root), self.repo.revparse_single('HEAD')) self.assertEqual(TRACKED_FP_CONTENTS_2, utils_lib.read_file(fp)) @assert_no_side_effects( TRACKED_FP, TRACKED_FP_WITH_SPACE, TRACKED_DIR_FP, TRACKED_DIR_FP_WITH_SPACE, TRACKED_DIR_DIR_FP, TRACKED_DIR_DIR_FP_WITH_SPACE) def test_checkout_head(self): self.__assert_checkout_head( TRACKED_FP, TRACKED_FP_WITH_SPACE, TRACKED_DIR_FP, TRACKED_DIR_FP_WITH_SPACE, TRACKED_DIR_DIR_FP, TRACKED_DIR_DIR_FP_WITH_SPACE) @assert_no_side_effects( TRACKED_FP, TRACKED_FP_WITH_SPACE, TRACKED_DIR_FP, TRACKED_DIR_FP_WITH_SPACE, TRACKED_DIR_DIR_FP, TRACKED_DIR_DIR_FP_WITH_SPACE) def test_checkout_head_relative(self): os.chdir(DIR) self.__assert_checkout_head(os.path.relpath(TRACKED_DIR_FP, DIR)) self.__assert_checkout_head(os.path.relpath(TRACKED_DIR_FP_WITH_SPACE, DIR)) os.chdir(DIR) self.__assert_checkout_head(os.path.relpath(TRACKED_DIR_DIR_FP, DIR_DIR)) self.__assert_checkout_head( os.path.relpath(TRACKED_DIR_DIR_FP_WITH_SPACE, DIR_DIR)) def __assert_checkout_not_head(self, *fps): root = self.repo.root for fp in fps: utils_lib.write_file(fp, contents='contents') self.curr_b.checkout_file( os.path.relpath(fp, root), self.repo.revparse_single('HEAD^')) self.assertEqual(TRACKED_FP_CONTENTS_1, utils_lib.read_file(fp)) def test_checkout_not_head(self): self.__assert_checkout_not_head( TRACKED_FP, TRACKED_FP_WITH_SPACE, TRACKED_DIR_FP, TRACKED_DIR_FP_WITH_SPACE, TRACKED_DIR_DIR_FP, TRACKED_DIR_DIR_FP_WITH_SPACE) def test_checkout_not_head_relative(self): os.chdir(DIR) self.__assert_checkout_not_head(os.path.relpath(TRACKED_DIR_FP, DIR)) self.__assert_checkout_not_head( os.path.relpath(TRACKED_DIR_FP_WITH_SPACE, DIR)) os.chdir(DIR) self.__assert_checkout_not_head( os.path.relpath(TRACKED_DIR_DIR_FP, DIR_DIR)) self.__assert_checkout_not_head( os.path.relpath(TRACKED_DIR_DIR_FP_WITH_SPACE, DIR_DIR)) def __assert_checkout_error(self, *fps, **kwargs): root = self.repo.root cp = kwargs.get('cp', 'HEAD') for fp in fps: self.assertRaises( KeyError, self.curr_b.checkout_file, os.path.relpath(fp, root), self.repo.revparse_single(cp)) @assert_no_side_effects( UNTRACKED_FP, UNTRACKED_FP_WITH_SPACE, UNTRACKED_DIR_FP, UNTRACKED_DIR_FP_WITH_SPACE, UNTRACKED_DIR_DIR_FP, UNTRACKED_DIR_DIR_FP_WITH_SPACE) def test_checkout_uncommitted(self): self.__assert_checkout_error( UNTRACKED_FP, UNTRACKED_FP_WITH_SPACE, UNTRACKED_DIR_FP, UNTRACKED_DIR_FP_WITH_SPACE, UNTRACKED_DIR_DIR_FP, UNTRACKED_DIR_DIR_FP_WITH_SPACE) self.__assert_checkout_error( UNTRACKED_FP, UNTRACKED_FP_WITH_SPACE, UNTRACKED_DIR_FP, UNTRACKED_DIR_FP_WITH_SPACE, UNTRACKED_DIR_DIR_FP, UNTRACKED_DIR_DIR_FP_WITH_SPACE, cp='HEAD^1') def test_checkout_nonexistent(self): self.__assert_checkout_error(NONEXISTENT_FP, NONEXISTENT_FP_WITH_SPACE) class TestFileStatus(TestFile): def test_status_all(self): st_all = self.curr_b.status() for fp, f_type, exists_at_head, exists_in_wd, modified, _ in st_all: if (fp == UNTRACKED_FP or fp == UNTRACKED_FP_WITH_SPACE or fp == UNTRACKED_DIR_FP or fp == UNTRACKED_DIR_FP_WITH_SPACE or fp == UNTRACKED_DIR_DIR_FP or fp == UNTRACKED_DIR_DIR_FP_WITH_SPACE): self.__assert_type(fp, core.GL_STATUS_UNTRACKED, f_type) self.__assert_field(fp, 'exists_at_head', False, exists_at_head) self.__assert_field(fp, 'modified', True, modified) elif fp == '.gitignore': self.__assert_type(fp, core.GL_STATUS_UNTRACKED, f_type) self.__assert_field(fp, 'exists_at_head', False, exists_at_head) self.__assert_field(fp, 'modified', True, modified) self.__assert_field(fp, 'exists_in_wd', True, exists_in_wd) def test_status_equivalence(self): for f_st in self.curr_b.status(): self.assertEqual(f_st, self.curr_b.status_file(f_st.fp)) def test_status_nonexistent_fp(self): self.assertRaises(KeyError, self.curr_b.status_file, NONEXISTENT_FP) self.assertRaises( KeyError, self.curr_b.status_file, NONEXISTENT_FP_WITH_SPACE) def test_status_modify(self): utils_lib.write_file(TRACKED_FP, contents='contents') st = self.curr_b.status_file(TRACKED_FP) self.assertTrue(st.modified) utils_lib.write_file(TRACKED_FP, contents=TRACKED_FP_CONTENTS_2) st = self.curr_b.status_file(TRACKED_FP) self.assertFalse(st.modified) def test_status_rm(self): os.remove(TRACKED_FP) st = self.curr_b.status_file(TRACKED_FP) self.assertEqual(core.GL_STATUS_TRACKED, st.type) self.assertTrue(st.modified) self.assertTrue(st.exists_at_head) self.assertFalse(st.exists_in_wd) utils_lib.write_file(TRACKED_FP, contents=TRACKED_FP_CONTENTS_2) st = self.curr_b.status_file(TRACKED_FP) self.assertEqual(core.GL_STATUS_TRACKED, st.type) self.assertFalse(st.modified) self.assertTrue(st.exists_at_head) self.assertTrue(st.exists_in_wd) def test_status_track_rm(self): self.curr_b.track_file(UNTRACKED_FP) st = self.curr_b.status_file(UNTRACKED_FP) self.assertEqual(core.GL_STATUS_TRACKED, st.type) self.assertTrue(st.modified) os.remove(UNTRACKED_FP) self.assertRaises(KeyError, self.curr_b.status_file, UNTRACKED_FP) def test_status_track_untrack(self): self.curr_b.track_file(UNTRACKED_FP) st = self.curr_b.status_file(UNTRACKED_FP) self.assertEqual(core.GL_STATUS_TRACKED, st.type) self.assertTrue(st.modified) self.curr_b.untrack_file(UNTRACKED_FP) st = self.curr_b.status_file(UNTRACKED_FP) self.assertEqual(core.GL_STATUS_UNTRACKED, st.type) self.assertTrue(st.modified) def test_status_unignore(self): utils_lib.write_file('.gitignore', contents='') st = self.curr_b.status_file(IGNORED_FP) self.assertEqual(core.GL_STATUS_UNTRACKED, st.type) st = self.curr_b.status_file(IGNORED_FP_WITH_SPACE) self.assertEqual(core.GL_STATUS_UNTRACKED, st.type) def test_status_ignore(self): contents = utils_lib.read_file('.gitignore') + '\n' + TRACKED_FP utils_lib.write_file('.gitignore', contents=contents) # Tracked files can't be ignored st = self.curr_b.status_file(TRACKED_FP) self.assertEqual(core.GL_STATUS_TRACKED, st.type) def test_status_untrack_tracked_modify(self): self.curr_b.untrack_file(TRACKED_FP) st = self.curr_b.status_file(TRACKED_FP) self.assertEqual(core.GL_STATUS_UNTRACKED, st.type) # self.assertFalse(st.modified) utils_lib.write_file(TRACKED_FP, contents='contents') st = self.curr_b.status_file(TRACKED_FP) self.assertEqual(core.GL_STATUS_UNTRACKED, st.type) self.assertTrue(st.modified) def test_status_untrack_tracked_rm(self): self.curr_b.untrack_file(TRACKED_FP) st = self.curr_b.status_file(TRACKED_FP) self.assertEqual(core.GL_STATUS_UNTRACKED, st.type) os.remove(TRACKED_FP) st = self.curr_b.status_file(TRACKED_FP) self.assertEqual(core.GL_STATUS_UNTRACKED, st.type) self.assertTrue(st.modified) self.assertFalse(st.exists_in_wd) self.assertTrue(st.exists_at_head) def test_status_ignore_tracked(self): """Assert that ignoring a tracked file has no effect.""" utils_lib.append_to_file('.gitignore', contents='\n' + TRACKED_FP + '\n') st = self.curr_b.status_file(TRACKED_FP) self.__assert_type(TRACKED_FP, core.GL_STATUS_TRACKED, st.type) def test_status_ignore_untracked(self): """Assert that ignoring a untracked file makes it ignored.""" utils_lib.append_to_file('.gitignore', contents='\n' + UNTRACKED_FP + '\n') st = self.curr_b.status_file(UNTRACKED_FP) self.__assert_type(UNTRACKED_FP, core.GL_STATUS_IGNORED, st.type) def __assert_type(self, fp, expected, got): self.assertEqual( expected, got, 'Incorrect type for {0}: expected {1}, got {2}'.format( fp, expected, got)) def __assert_field(self, fp, field, expected, got): self.assertEqual( expected, got, 'Incorrect status for {0}: expected {1}={2}, got {3}={4}'.format( fp, field, expected, field, got)) class TestFileDiff(TestFile): @assert_status_unchanged( UNTRACKED_FP, UNTRACKED_FP_WITH_SPACE, IGNORED_FP, IGNORED_FP_WITH_SPACE) def test_diff_nontracked(self): fps = [ UNTRACKED_FP, UNTRACKED_FP_WITH_SPACE, IGNORED_FP, IGNORED_FP_WITH_SPACE] for fp in fps: utils_lib.write_file(fp, contents='new contents') patch = self.curr_b.diff_file(fp) self.assertEqual(1, patch.line_stats[1]) self.assertEqual(0, patch.line_stats[2]) self.assertEqual(1, len(patch.hunks)) hunk = list(patch.hunks)[0] self.assertEqual(2, len(hunk.lines)) self.assertEqual('+', hunk.lines[0].origin) self.assertEqual('new contents', hunk.lines[0].content) def test_diff_nonexistent_fp(self): self.assertRaises(KeyError, self.curr_b.diff_file, NONEXISTENT_FP) self.assertRaises( KeyError, self.curr_b.diff_file, NONEXISTENT_FP_WITH_SPACE) @assert_no_side_effects(TRACKED_FP) def test_empty_diff(self): patch = self.curr_b.diff_file(TRACKED_FP) self.assertEqual(0, len(list(patch.hunks))) self.assertEqual(0, patch.line_stats[1]) self.assertEqual(0, patch.line_stats[2]) def test_diff_basic(self): utils_lib.write_file(TRACKED_FP, contents='new contents') patch = self.curr_b.diff_file(TRACKED_FP) self.assertEqual(1, patch.line_stats[1]) self.assertEqual(1, patch.line_stats[2]) self.assertEqual(1, len(patch.hunks)) hunk = list(patch.hunks)[0] self.assertEqual(3, len(hunk.lines)) self.assertEqual('-', hunk.lines[0].origin) self.assertEqual(TRACKED_FP_CONTENTS_2, hunk.lines[0].content) self.assertEqual('+', hunk.lines[1].origin) self.assertEqual('new contents', hunk.lines[1].content) def test_diff_append(self): utils_lib.append_to_file(TRACKED_FP, contents='new contents') patch = self.curr_b.diff_file(TRACKED_FP) self.assertEqual(1, patch.line_stats[1]) self.assertEqual(0, patch.line_stats[2]) self.assertEqual(1, len(patch.hunks)) hunk = list(patch.hunks)[0] self.assertEqual(3, len(hunk.lines)) self.assertEqual(' ', hunk.lines[0].origin) self.assertEqual(TRACKED_FP_CONTENTS_2, hunk.lines[0].content) self.assertEqual('+', hunk.lines[1].origin) self.assertEqual('new contents', hunk.lines[1].content) def test_diff_new_fp(self): fp = 'new' new_fp_contents = 'new fp contents\n' utils_lib.write_file(fp, contents=new_fp_contents) self.curr_b.track_file(fp) patch = self.curr_b.diff_file(fp) self.assertEqual(1, patch.line_stats[1]) self.assertEqual(0, patch.line_stats[2]) self.assertEqual(1, len(patch.hunks)) hunk = list(patch.hunks)[0] self.assertEqual(1, len(hunk.lines)) self.assertEqual('+', hunk.lines[0].origin) self.assertEqual(new_fp_contents, hunk.lines[0].content) # Now let's add some change to the file and check that diff notices it utils_lib.append_to_file(fp, contents='new line') patch = self.curr_b.diff_file(fp) self.assertEqual(2, patch.line_stats[1]) self.assertEqual(0, patch.line_stats[2]) self.assertEqual(1, len(patch.hunks)) hunk = list(patch.hunks)[0] self.assertEqual(3, len(hunk.lines)) self.assertEqual('+', hunk.lines[0].origin) self.assertEqual(new_fp_contents, hunk.lines[0].content) self.assertEqual('+', hunk.lines[1].origin) self.assertEqual('new line', hunk.lines[1].content) def test_diff_non_ascii(self): if sys.platform == 'win32': # Skip this test on Windows until we fix Unicode support return fp = 'new' new_fp_contents = '’◕‿◕’©Ä☺’ಠ_ಠ’\n' utils_lib.write_file(fp, contents=new_fp_contents) self.curr_b.track_file(fp) patch = self.curr_b.diff_file(fp) self.assertEqual(1, patch.line_stats[1]) self.assertEqual(0, patch.line_stats[2]) self.assertEqual(1, len(patch.hunks)) hunk = list(patch.hunks)[0] self.assertEqual(1, len(hunk.lines)) self.assertEqual('+', hunk.lines[0].origin) self.assertEqual(new_fp_contents, hunk.lines[0].content) utils_lib.append_to_file(fp, contents='new line') patch = self.curr_b.diff_file(fp) self.assertEqual(2, patch.line_stats[1]) self.assertEqual(0, patch.line_stats[2]) self.assertEqual(1, len(patch.hunks)) hunk = list(patch.hunks)[0] self.assertEqual(3, len(hunk.lines)) self.assertEqual('+', hunk.lines[0].origin) self.assertEqual(new_fp_contents, hunk.lines[0].content) self.assertEqual('+', hunk.lines[1].origin) self.assertEqual('new line', hunk.lines[1].content) class TestFileResolve(TestFile): def setUp(self): super(TestFileResolve, self).setUp() # Generate a conflict git.checkout(b='branch') utils_lib.write_file(FP_IN_CONFLICT, contents='branch') utils_lib.write_file(DIR_FP_IN_CONFLICT, contents='branch') git.add(FP_IN_CONFLICT, DIR_FP_IN_CONFLICT) git.commit(FP_IN_CONFLICT, DIR_FP_IN_CONFLICT, m='branch') git.checkout('master') utils_lib.write_file(FP_IN_CONFLICT, contents='master') utils_lib.write_file(DIR_FP_IN_CONFLICT, contents='master') git.add(FP_IN_CONFLICT, DIR_FP_IN_CONFLICT) git.commit(FP_IN_CONFLICT, DIR_FP_IN_CONFLICT, m='master') git.merge('branch', _ok_code=[1]) @assert_no_side_effects(TRACKED_FP) def test_resolve_fp_with_no_conflicts(self): self.assertRaisesRegexp( ValueError, 'no conflicts', self.curr_b.resolve_file, TRACKED_FP) def __assert_resolve_fp(self, *fps): for fp in fps: self.curr_b.resolve_file(fp) st = self.curr_b.status_file(fp) self.assertFalse(st.in_conflict) @assert_contents_unchanged(FP_IN_CONFLICT, DIR_FP_IN_CONFLICT) def test_resolve_fp_with_conflicts(self): self.__assert_resolve_fp(FP_IN_CONFLICT, DIR_FP_IN_CONFLICT) def test_resolve_relative(self): self.__assert_resolve_fp(DIR_FP_IN_CONFLICT) os.chdir(DIR) st = self.curr_b.status_file(DIR_FP_IN_CONFLICT) self.assertFalse(st.in_conflict) self.assertRaisesRegexp( ValueError, 'no conflicts', self.curr_b.resolve_file, DIR_FP_IN_CONFLICT) # Unit tests for branch related operations class TestBranch(TestCore): """Base class for branch tests.""" def setUp(self): super(TestBranch, self).setUp() # Build up an interesting mock repo. utils_lib.write_file(TRACKED_FP, contents=TRACKED_FP_CONTENTS_1) git.add(TRACKED_FP) git.commit(TRACKED_FP, m='1') utils_lib.write_file(TRACKED_FP, contents=TRACKED_FP_CONTENTS_2) git.commit(TRACKED_FP, m='2') utils_lib.write_file(UNTRACKED_FP, contents=UNTRACKED_FP_CONTENTS) utils_lib.write_file('.gitignore', contents='{0}'.format(IGNORED_FP)) utils_lib.write_file(IGNORED_FP) git.branch(BRANCH) self.curr_b = self.repo.current_branch class TestBranchCreate(TestBranch): def _assert_value_error(self, name, regexp): self.assertRaisesRegexp( ValueError, regexp, self.repo.create_branch, name, self.repo.current_branch.head) def test_create_invalid_name(self): assert_invalid_name = lambda n: self._assert_value_error(n, 'not valid') assert_invalid_name('') assert_invalid_name('\t') assert_invalid_name(' ') def test_create_existent_name(self): self.repo.create_branch('branch1', self.repo.current_branch.head) self._assert_value_error('branch1', 'exists') def test_create(self): self.repo.create_branch('branch1', self.repo.current_branch.head) self.repo.switch_current_branch(self.repo.lookup_branch('branch1')) self.assertTrue(os.path.exists(TRACKED_FP)) self.assertEqual(TRACKED_FP_CONTENTS_2, utils_lib.read_file(TRACKED_FP)) self.assertFalse(os.path.exists(UNTRACKED_FP)) self.assertFalse(os.path.exists(IGNORED_FP)) self.assertFalse(os.path.exists('.gitignore')) def test_create_from_prev_commit(self): self.repo.create_branch('branch1', self.repo.revparse_single('HEAD^')) self.repo.switch_current_branch(self.repo.lookup_branch('branch1')) self.assertTrue(os.path.exists(TRACKED_FP)) self.assertEqual(TRACKED_FP_CONTENTS_1, utils_lib.read_file(TRACKED_FP)) self.assertFalse(os.path.exists(UNTRACKED_FP)) self.assertFalse(os.path.exists(IGNORED_FP)) self.assertFalse(os.path.exists('.gitignore')) class TestBranchDelete(TestBranch): def test_delete(self): self.repo.lookup_branch(BRANCH).delete() self.assertRaises( core.BranchIsCurrentError, self.repo.lookup_branch('master').delete) class TestBranchSwitch(TestBranch): def test_switch_contents_still_there_untrack_tracked(self): self.curr_b.untrack_file(TRACKED_FP) utils_lib.write_file(TRACKED_FP, contents='contents') self.repo.switch_current_branch(self.repo.lookup_branch(BRANCH)) self.assertEqual(TRACKED_FP_CONTENTS_2, utils_lib.read_file(TRACKED_FP)) self.repo.switch_current_branch(self.repo.lookup_branch('master')) self.assertEqual('contents', utils_lib.read_file(TRACKED_FP)) def test_switch_contents_still_there_untracked(self): self.repo.switch_current_branch(self.repo.lookup_branch(BRANCH)) utils_lib.write_file(UNTRACKED_FP, contents='contents') self.repo.switch_current_branch(self.repo.lookup_branch('master')) self.assertEqual(UNTRACKED_FP_CONTENTS, utils_lib.read_file(UNTRACKED_FP)) self.repo.switch_current_branch(self.repo.lookup_branch(BRANCH)) self.assertEqual('contents', utils_lib.read_file(UNTRACKED_FP)) def test_switch_contents_still_there_ignored(self): self.repo.switch_current_branch(self.repo.lookup_branch(BRANCH)) utils_lib.write_file(IGNORED_FP, contents='contents') self.repo.switch_current_branch(self.repo.lookup_branch('master')) self.assertEqual(IGNORED_FP, utils_lib.read_file(IGNORED_FP)) self.repo.switch_current_branch(self.repo.lookup_branch(BRANCH)) self.assertEqual('contents', utils_lib.read_file(IGNORED_FP)) def test_switch_contents_still_there_tracked_commit(self): utils_lib.write_file(TRACKED_FP, contents='commit') git.commit(TRACKED_FP, m='comment') self.repo.switch_current_branch(self.repo.lookup_branch(BRANCH)) self.assertEqual(TRACKED_FP_CONTENTS_2, utils_lib.read_file(TRACKED_FP)) self.repo.switch_current_branch(self.repo.lookup_branch('master')) self.assertEqual('commit', utils_lib.read_file(TRACKED_FP)) def test_switch_file_classification_is_mantained(self): self.curr_b.untrack_file(TRACKED_FP) self.repo.switch_current_branch(self.repo.lookup_branch(BRANCH)) st = self.curr_b.status_file(TRACKED_FP) self.assertTrue(st) self.assertEqual(core.GL_STATUS_TRACKED, st.type) self.repo.switch_current_branch(self.repo.lookup_branch('master')) st = self.curr_b.status_file(TRACKED_FP) self.assertTrue(st) self.assertEqual(core.GL_STATUS_UNTRACKED, st.type) def test_switch_with_hidden_files(self): hf = '.file' utils_lib.write_file(hf) self.repo.switch_current_branch(self.repo.lookup_branch(BRANCH)) utils_lib.write_file(hf, contents='contents') self.repo.switch_current_branch(self.repo.lookup_branch('master')) self.assertEqual(hf, utils_lib.read_file(hf)) self.repo.switch_current_branch(self.repo.lookup_branch(BRANCH)) self.assertEqual('contents', utils_lib.read_file(hf)) # Unit tests for remote related operations class TestRemote(TestCore): """Base class for remote tests.""" def setUp(self): """Creates temporary local Git repo to use as the remote.""" super(TestRemote, self).setUp() # Create a repo to use as the remote self.remote_path = tempfile.mkdtemp(prefix='gl-remote-test') os.chdir(self.remote_path) remote_repo = core.init_repository() remote_repo.create_branch( REMOTE_BRANCH, remote_repo.revparse_single('HEAD')) # Go back to the original repo os.chdir(self.path) self.remotes = self.repo.remotes def tearDown(self): """Removes the temporary dir.""" super(TestRemote, self).tearDown() utils_lib.rmtree(self.remote_path) class TestRemoteCreate(TestRemote): def test_create_new(self): self.remotes.create('remote', self.remote_path) def test_create_existing(self): self.remotes.create('remote', self.remote_path) self.assertRaises( ValueError, self.remotes.create, 'remote', self.remote_path) def test_create_invalid_name(self): self.assertRaises(ValueError, self.remotes.create, 'rem/ote', 'url') def test_create_invalid_url(self): self.assertRaises(ValueError, self.remotes.create, 'remote', '') class TestRemoteList(TestRemote): def test_list_all(self): self.remotes.create('remote1', self.remote_path) self.remotes.create('remote2', self.remote_path) self.assertItemsEqual( ['remote1', 'remote2'], [r.name for r in self.remotes]) class TestRemoteDelete(TestRemote): def test_delete(self): self.remotes.create('remote', self.remote_path) self.remotes.delete('remote') def test_delete_nonexistent(self): self.assertRaises(KeyError, self.remotes.delete, 'remote') self.remotes.create('remote', self.remote_path) self.remotes.delete('remote') self.assertRaises(KeyError, self.remotes.delete, 'remote') class TestRemoteSync(TestRemote): def setUp(self): super(TestRemoteSync, self).setUp() utils_lib.write_file('foo', contents='foo') git.add('foo') git.commit('foo', m='msg') self.repo.remotes.create('remote', self.remote_path) self.remote = self.repo.remotes['remote'] def test_sync_changes(self): master_head_before = self.remote.lookup_branch('master').head remote_branch = self.remote.lookup_branch(REMOTE_BRANCH) remote_branch_head_before = remote_branch.head current_b = self.repo.current_branch # It is not a ff so it should fail self.assertRaises(core.GlError, current_b.publish, remote_branch) # Get the changes git.rebase(remote_branch) # Retry (this time it should work) current_b.publish(remote_branch) self.assertItemsEqual( ['master', REMOTE_BRANCH], self.remote.listall_branches()) self.assertEqual( master_head_before.id, self.remote.lookup_branch('master').head.id) self.assertNotEqual( remote_branch_head_before.id, remote_branch.head.id) self.assertEqual(current_b.head.id, remote_branch.head.id) gitless-0.8.8/gitless/tests/test_e2e.py000077500000000000000000000564431347331605400201260ustar00rootroot00000000000000# -*- coding: utf-8 -*- # Gitless - a version control system built on top of Git # Licensed under MIT """End-to-end test.""" from __future__ import unicode_literals import logging import os import re import time import sys if sys.platform != 'win32': from sh import ErrorReturnCode, Command else: from pbs import ErrorReturnCode, Command gl = Command('gl') git = Command('git') from gitless.tests import utils try: text = unicode except NameError: text = str class TestEndToEnd(utils.TestBase): def setUp(self): super(TestEndToEnd, self).setUp('gl-e2e-test') gl.init() # Disable colored output so that we don't need to worry about ANSI escape # codes git.config('color.ui', False) # Disable paging so that we don't have to use sh's _tty_out option, which is # not available on pbs if sys.platform != 'win32': git.config('core.pager', 'cat') else: # On Windows, we need to call 'type' through cmd.exe (with 'cmd'). The /C # is so that the command window gets closed after 'type' finishes git.config('core.pager', 'cmd /C type') utils.set_test_config() class TestNotInRepo(utils.TestBase): def setUp(self): super(TestNotInRepo, self).setUp('gl-e2e-test') def test_not_in_repo(self): def assert_not_in_repo(*cmds): for cmd in cmds: self.assertRaisesRegexp( ErrorReturnCode, 'not in a Gitless\'s repository', cmd) assert_not_in_repo( gl.status, gl.diff, gl.commit, gl.branch, gl.merge, gl.fuse, gl.remote, gl.publish, gl.history) class TestBasic(TestEndToEnd): def test_basic_functionality(self): utils.write_file('file1', 'Contents of file1') # Track gl.track('file1') self.assertRaises(ErrorReturnCode, gl.track, 'file1') self.assertRaises(ErrorReturnCode, gl.track, 'non-existent') # Untrack gl.untrack('file1') self.assertRaises(ErrorReturnCode, gl.untrack, 'file1') self.assertRaises(ErrorReturnCode, gl.untrack, 'non-existent') # Commit gl.track('file1') gl.commit(m='file1 commit') self.assertRaises(ErrorReturnCode, gl.commit, m='nothing to commit') # History if 'file1 commit' not in utils.stdout(gl.history()): self.fail('Commit didn\'t appear in history') # Branch # Make some changes to file1 and branch out utils.write_file('file1', 'New contents of file1') gl.branch(c='branch1') gl.switch('branch1') if 'New' in utils.read_file('file1'): self.fail('Branch not independent!') # Switch back to master branch, check that contents are the same as before. gl.switch('master') if 'New' not in utils.read_file('file1'): self.fail('Branch not independent!') out = utils.stdout(gl.branch()) if '* master' not in out: self.fail('Branch status output wrong: {0}'.format(out)) if 'branch1' not in out: self.fail('Branch status output wrong: {0}'.format(out)) gl.branch(c='branch2') gl.branch(c='branch-conflict1') gl.branch(c='branch-conflict2') gl.commit(m='New contents commit') # Fuse gl.switch('branch1') self.assertRaises(ErrorReturnCode, gl.fuse) # no upstream set try: gl.fuse('master') except ErrorReturnCode as e: self.fail(utils.stderr(e)) out = utils.stdout(gl.history()) if 'file1 commit' not in out: self.fail(out) # Merge gl.switch('branch2') self.assertRaises(ErrorReturnCode, gl.merge) # no upstream set gl.merge('master') out = utils.stdout(gl.history()) if 'file1 commit' not in out: self.fail(out) # Conflicting fuse gl.switch('branch-conflict1') utils.write_file('file1', 'Conflicting changes to file1') gl.commit(m='changes in branch-conflict1') err = utils.stderr(gl.fuse('master', _ok_code=[1])) if 'conflict' not in err: self.fail(err) out = utils.stdout(gl.status()) if 'file1 (with conflicts)' not in out: self.fail(out) # Try aborting gl.fuse('--abort') out = utils.stdout(gl.status()) if 'file1' in out: self.fail(out) # Ok, now let's fix the conflicts err = utils.stderr(gl.fuse('master', _ok_code=[1])) if 'conflict' not in err: self.fail(err) out = utils.stdout(gl.status()) if 'file1 (with conflicts)' not in out: self.fail(out) utils.write_file('file1', 'Fixed conflicts!') self.assertRaises(ErrorReturnCode, gl.commit, m='resolve not called') self.assertRaises(ErrorReturnCode, gl.resolve, 'non-existent') gl.resolve('file1') gl.commit(m='fixed conflicts') class TestCommit(TestEndToEnd): TRACKED_FP = 'file1' DIR_TRACKED_FP = 'dir/dir_file' UNTRACKED_FP = 'file2' FPS = [TRACKED_FP, DIR_TRACKED_FP, UNTRACKED_FP] DIR = 'dir' def setUp(self): super(TestCommit, self).setUp() utils.write_file(self.TRACKED_FP) utils.write_file(self.DIR_TRACKED_FP) utils.write_file(self.UNTRACKED_FP) gl.track(self.TRACKED_FP, self.DIR_TRACKED_FP) def test_commit(self): gl.commit(m='msg') self.__assert_commit(self.TRACKED_FP, self.DIR_TRACKED_FP) def test_commit_relative(self): os.chdir(self.DIR) gl.commit(m='msg') self.__assert_commit(self.TRACKED_FP, self.DIR_TRACKED_FP) def test_commit_only(self): gl.commit(self.TRACKED_FP, m="msg") self.__assert_commit(self.TRACKED_FP) def test_commit_only_relative(self): os.chdir(self.DIR) self.assertRaises(ErrorReturnCode, gl.commit, self.TRACKED_FP, "-m='msg'") gl.commit('../' + self.TRACKED_FP, m='msg') self.__assert_commit(self.TRACKED_FP) def test_commit_only_untrack(self): gl.commit("-m='msg'", self.UNTRACKED_FP) self.__assert_commit(self.UNTRACKED_FP) def test_commit_only_untrack_relative(self): os.chdir(self.DIR) self.assertRaises(ErrorReturnCode, gl.commit, self.UNTRACKED_FP, m='msg') gl.commit('../' + self.UNTRACKED_FP, m='msg') self.__assert_commit(self.UNTRACKED_FP) def test_commit_include(self): gl.commit("-m='msg'", include=self.UNTRACKED_FP) self.__assert_commit( self.TRACKED_FP, self.DIR_TRACKED_FP, self.UNTRACKED_FP) def test_commit_exclude_include(self): gl.commit("-m='msg'", include=self.UNTRACKED_FP, exclude=self.TRACKED_FP) self.__assert_commit(self.UNTRACKED_FP, self.DIR_TRACKED_FP) def test_commit_no_files(self): self.assertRaises( ErrorReturnCode, gl.commit, '--exclude', self.TRACKED_FP, self.DIR_TRACKED_FP, m='msg') self.assertRaises(ErrorReturnCode, gl.commit, 'non-existent', m='msg') self.assertRaises( ErrorReturnCode, gl.commit, m='msg', exclude='non-existent') self.assertRaises( ErrorReturnCode, gl.commit, m='msg', include='non-existent') def test_commit_dir(self): fp = 'dir/f' utils.write_file(fp) gl.commit(fp, m='msg') self.__assert_commit('dir/f') def __assert_commit(self, *expected_committed): h = utils.stdout(gl.history(v=True)) for fp in expected_committed: if fp not in h: self.fail('{0} was apparently not committed!'.format(fp)) expected_not_committed = [ fp for fp in self.FPS if fp not in expected_committed] for fp in expected_not_committed: if fp in h: self.fail('{0} was apparently committed!'.format(fp)) class TestStatus(TestEndToEnd): DIR = 'dir' TRACKED_DIR_FP = os.path.join('dir', 'file1') UNTRACKED_DIR_FP = os.path.join('dir', 'file2') def setUp(self): super(TestStatus, self).setUp() utils.write_file(self.TRACKED_DIR_FP) utils.write_file(self.UNTRACKED_DIR_FP) gl.commit(self.TRACKED_DIR_FP, m='commit') def test_status_relative(self): utils.write_file(self.TRACKED_DIR_FP, contents='some modifications') st = utils.stdout(gl.status()) if self.TRACKED_DIR_FP not in st: self.fail() if self.UNTRACKED_DIR_FP not in st: self.fail() os.chdir(self.DIR) st = utils.stdout(gl.status()) rel_tracked = os.path.relpath(self.TRACKED_DIR_FP, self.DIR) rel_untracked = os.path.relpath(self.UNTRACKED_DIR_FP, self.DIR) if (self.TRACKED_DIR_FP in st) or (rel_tracked not in st): self.fail() if (self.UNTRACKED_DIR_FP in st) or (rel_untracked not in st): self.fail() class TestBranch(TestEndToEnd): BRANCH_1 = 'branch1' BRANCH_2 = 'branch2' def setUp(self): super(TestBranch, self).setUp() utils.write_file('f') gl.commit('f', m='commit') def test_create(self): gl.branch(c=self.BRANCH_1) self.assertRaises(ErrorReturnCode, gl.branch, c=self.BRANCH_1) self.assertRaises(ErrorReturnCode, gl.branch, c='evil*named*branch') if self.BRANCH_1 not in utils.stdout(gl.branch()): self.fail() def test_remove(self): gl.branch(c=self.BRANCH_1) gl.switch(self.BRANCH_1) self.assertRaises(ErrorReturnCode, gl.branch, d=self.BRANCH_1, _in='y') gl.branch(c=self.BRANCH_2) gl.switch(self.BRANCH_2) gl.branch(d=self.BRANCH_1, _in='n') gl.branch(d=self.BRANCH_1, _in='y') if self.BRANCH_1 in utils.stdout(gl.branch()): self.fail() def test_upstream(self): self.assertRaises(ErrorReturnCode, gl.branch, '-uu') self.assertRaises(ErrorReturnCode, gl.branch, '-su', 'non-existent') self.assertRaises( ErrorReturnCode, gl.branch, '-su', 'non-existent/non-existent') def test_list(self): gl.branch(c=self.BRANCH_1) gl.branch(c=self.BRANCH_2) branch_out = utils.stdout(gl.branch()) self.assertTrue( branch_out.find(self.BRANCH_1) < branch_out.find(self.BRANCH_2)) class TestTag(TestEndToEnd): TAG_1 = 'tag1' TAG_2 = 'tag2' def setUp(self): super(TestTag, self).setUp() utils.write_file('f') gl.commit('f', m='commit') def test_create(self): gl.tag(c=self.TAG_1) self.assertRaises(ErrorReturnCode, gl.tag, c=self.TAG_1) self.assertRaises(ErrorReturnCode, gl.tag, c='evil*named*tag') if self.TAG_1 not in utils.stdout(gl.tag()): self.fail() def test_remove(self): gl.tag(c=self.TAG_1) gl.tag(d=self.TAG_1, _in='n') gl.tag(d=self.TAG_1, _in='y') if self.TAG_1 in utils.stdout(gl.tag()): self.fail() def test_list(self): gl.tag(c=self.TAG_1) gl.tag(c=self.TAG_2) tag_out = utils.stdout(gl.tag()) self.assertTrue( tag_out.find(self.TAG_1) < tag_out.find(self.TAG_2)) class TestDiffFile(TestEndToEnd): TRACKED_FP = 't_fp' DIR_TRACKED_FP = os.path.join('dir', 't_fp') UNTRACKED_FP = 'u_fp' DIR = 'dir' def setUp(self): super(TestDiffFile, self).setUp() utils.write_file(self.TRACKED_FP) utils.write_file(self.DIR_TRACKED_FP) gl.commit(self.TRACKED_FP, self.DIR_TRACKED_FP, m='commit') utils.write_file(self.UNTRACKED_FP) def test_empty_diff(self): if 'No files to diff' not in utils.stdout(gl.diff()): self.fail() def test_diff_nonexistent_fp(self): err = utils.stderr(gl.diff('file', _ok_code=[1])) if 'doesn\'t exist' not in err: self.fail() def test_basic_diff(self): utils.write_file(self.TRACKED_FP, contents='contents') out1 = utils.stdout(gl.diff()) if '+contents' not in out1: self.fail() out2 = utils.stdout(gl.diff(self.TRACKED_FP)) if '+contents' not in out2: self.fail() self.assertEqual(out1, out2) def test_basic_diff_relative(self): utils.write_file(self.TRACKED_FP, contents='contents_tracked') utils.write_file(self.DIR_TRACKED_FP, contents='contents_dir_tracked') os.chdir(self.DIR) out1 = utils.stdout(gl.diff()) if '+contents_tracked' not in out1: self.fail() if '+contents_dir_tracked' not in out1: self.fail() rel_dir_tracked_fp = os.path.relpath(self.DIR_TRACKED_FP, self.DIR) out2 = utils.stdout(gl.diff(rel_dir_tracked_fp)) if '+contents_dir_tracked' not in out2: self.fail() def test_diff_dir(self): fp = 'dir/dir/f' utils.write_file(fp, contents='contents') out = utils.stdout(gl.diff(fp)) if '+contents' not in out: self.fail() def test_diff_non_ascii(self): if sys.platform == 'win32': # Skip this test on Windows until we fix Unicode support return contents = '’◕‿◕’©Ä☺’ಠ_ಠ’' utils.write_file(self.TRACKED_FP, contents=contents) out1 = utils.stdout(gl.diff()) if '+' + contents not in out1: self.fail('out is ' + out1) out2 = utils.stdout(gl.diff(self.TRACKED_FP)) if '+' + contents not in out2: self.fail('out is ' + out2) self.assertEqual(out1, out2) class TestOp(TestEndToEnd): COMMITS_NUMBER = 4 OTHER = 'other' MASTER_FILE = 'master_file' OTHER_FILE = 'other_file' def setUp(self): super(TestOp, self).setUp() self.commits = {} def create_commits(branch_name, fp): self.commits[branch_name] = [] utils.append_to_file(fp, contents='contents {0}\n'.format(0)) out = utils.stdout(gl.commit(m='ci 0 in {0}'.format(branch_name), inc=fp)) self.commits[branch_name].append( re.search(r'Commit Id: (\S*)', out, re.UNICODE).group(1)) for i in range(1, self.COMMITS_NUMBER): utils.append_to_file(fp, contents='contents {0}\n'.format(i)) out = utils.stdout(gl.commit(m='ci {0} in {1}'.format(i, branch_name))) self.commits[branch_name].append( re.search(r'Commit Id: (\S*)', out, re.UNICODE).group(1)) gl.branch(c=self.OTHER) create_commits('master', self.MASTER_FILE) gl.switch(self.OTHER) create_commits(self.OTHER, self.OTHER_FILE) gl.switch('master') class TestFuse(TestOp): def __assert_history(self, expected): out = utils.stdout(gl.history()) cids = list(reversed(re.findall(r'ci (.*) in (\S*)', out, re.UNICODE))) self.assertItemsEqual( cids, expected, 'cids is ' + text(cids) + ' exp ' + text(expected)) st_out = utils.stdout(gl.status()) self.assertFalse('fuse' in st_out) def __build(self, branch_name, cids=None): if not cids: cids = range(self.COMMITS_NUMBER) return [(text(ci), branch_name) for ci in cids] def test_basic(self): gl.fuse(self.OTHER) self.__assert_history(self.__build(self.OTHER) + self.__build('master')) def test_only_errors(self): self.assertRaises(ErrorReturnCode, gl.fuse, self.OTHER, o='non-existent-id') self.assertRaises( ErrorReturnCode, gl.fuse, self.OTHER, o=self.commits['master'][1]) def test_only_one(self): gl.fuse(self.OTHER, o=self.commits[self.OTHER][0]) self.__assert_history( self.__build(self.OTHER, cids=[0]) + self.__build('master')) def test_only_some(self): gl.fuse(self.OTHER, '-o', self.commits[self.OTHER][:2]) self.__assert_history( self.__build(self.OTHER, [0, 1]) + self.__build('master')) def test_exclude_errors(self): self.assertRaises(ErrorReturnCode, gl.fuse, self.OTHER, e='non-existent-id') self.assertRaises( ErrorReturnCode, gl.fuse, self.OTHER, e=self.commits['master'][1]) def test_exclude_one(self): last_ci = self.COMMITS_NUMBER - 1 gl.fuse(self.OTHER, e=self.commits[self.OTHER][last_ci]) self.__assert_history( self.__build(self.OTHER, range(0, last_ci)) + self.__build('master')) def test_exclude_some(self): gl.fuse(self.OTHER, '-e', self.commits[self.OTHER][1:]) self.__assert_history( self.__build(self.OTHER, cids=[0]) + self.__build('master')) def test_ip_dp(self): gl.fuse(self.OTHER, insertion_point='dp') self.__assert_history(self.__build(self.OTHER) + self.__build('master')) def test_ip_head(self): gl.fuse(self.OTHER, insertion_point='HEAD') self.__assert_history(self.__build('master') + self.__build(self.OTHER)) def test_ip_commit(self): gl.fuse(self.OTHER, insertion_point=self.commits['master'][1]) self.__assert_history( self.__build('master', [0, 1]) + self.__build(self.OTHER) + self.__build('master', range(2, self.COMMITS_NUMBER))) def test_conflicts(self): def trigger_conflicts(): self.assertRaisesRegexp( ErrorReturnCode, 'conflicts', gl.fuse, self.OTHER, e=self.commits[self.OTHER][0]) # Abort trigger_conflicts() gl.fuse('-a') self.__assert_history(self.__build('master')) # Fix conflicts trigger_conflicts() gl.resolve(self.OTHER_FILE) gl.commit(m='ci 1 in other') self.__assert_history( self.__build(self.OTHER, range(1, self.COMMITS_NUMBER)) + self.__build('master')) def test_conflicts_switch(self): gl.switch('other') utils.write_file(self.OTHER_FILE, contents='uncommitted') gl.switch('master') try: gl.fuse(self.OTHER, e=self.commits[self.OTHER][0]) self.fail() except ErrorReturnCode: pass # Switch gl.switch('other') self.__assert_history(self.__build('other')) st_out = utils.stdout(gl.status()) self.assertTrue('fuse' not in st_out) self.assertTrue('conflict' not in st_out) gl.switch('master') st_out = utils.stdout(gl.status()) self.assertTrue('fuse' in st_out) self.assertTrue('conflict' in st_out) # Check that we are able to complete the fuse after switch gl.resolve(self.OTHER_FILE) gl.commit(m='ci 1 in other') self.__assert_history( self.__build(self.OTHER, range(1, self.COMMITS_NUMBER)) + self.__build('master')) gl.switch('other') self.assertEqual('uncommitted', utils.read_file(self.OTHER_FILE)) def test_conflicts_multiple(self): gl.branch(c='tmp', divergent_point='HEAD~2') gl.switch('tmp') utils.append_to_file(self.MASTER_FILE, contents='conflict') gl.commit(m='will conflict 0') utils.append_to_file(self.MASTER_FILE, contents='conflict') gl.commit(m='will conflict 1') self.assertRaisesRegexp(ErrorReturnCode, 'conflicts', gl.fuse, 'master') gl.resolve(self.MASTER_FILE) self.assertRaisesRegexp( ErrorReturnCode, 'conflicts', gl.commit, m='ci 0 in tmp') gl.resolve(self.MASTER_FILE) gl.commit(m='ci 1 in tmp') # this one should finalize the fuse self.__assert_history( self.__build('master') + self.__build('tmp', range(2))) def test_conflicts_multiple_uncommitted_changes(self): gl.branch(c='tmp', divergent_point='HEAD~2') gl.switch('tmp') utils.append_to_file(self.MASTER_FILE, contents='conflict') gl.commit(m='will conflict 0') utils.append_to_file(self.MASTER_FILE, contents='conflict') gl.commit(m='will conflict 1') utils.write_file(self.MASTER_FILE, contents='uncommitted') self.assertRaisesRegexp(ErrorReturnCode, 'conflicts', gl.fuse, 'master') gl.resolve(self.MASTER_FILE) self.assertRaisesRegexp( ErrorReturnCode, 'conflicts', gl.commit, m='ci 0 in tmp') gl.resolve(self.MASTER_FILE) self.assertRaisesRegexp( ErrorReturnCode, 'failed to apply', gl.commit, m='ci 1 in tmp') self.__assert_history( self.__build('master') + self.__build('tmp', range(2))) self.assertTrue('Stashed' in utils.read_file(self.MASTER_FILE)) def test_nothing_to_fuse(self): self.assertRaisesRegexp( ErrorReturnCode, 'No commits to fuse', gl.fuse, self.OTHER, '-e', *self.commits[self.OTHER]) def test_ff(self): gl.branch(c='tmp', divergent_point='HEAD~2') gl.switch('tmp') gl.fuse('master') self.__assert_history(self.__build('master')) def test_ff_ip_head(self): gl.branch(c='tmp', divergent_point='HEAD~2') gl.switch('tmp') gl.fuse('master', insertion_point='HEAD') self.__assert_history(self.__build('master')) def test_uncommitted_changes(self): utils.write_file(self.MASTER_FILE, contents='uncommitted') utils.write_file('master_untracked', contents='uncommitted') gl.fuse(self.OTHER) self.assertEqual('uncommitted', utils.read_file(self.MASTER_FILE)) self.assertEqual('uncommitted', utils.read_file('master_untracked')) def test_uncommitted_tracked_changes_that_conflict(self): gl.branch(c='tmp', divergent_point='HEAD~1') gl.switch('tmp') utils.write_file(self.MASTER_FILE, contents='uncommitted') self.assertRaisesRegexp( ErrorReturnCode, 'failed to apply', gl.fuse, 'master', insertion_point='HEAD') contents = utils.read_file(self.MASTER_FILE) self.assertTrue('uncommitted' in contents) self.assertTrue('contents 2' in contents) def test_uncommitted_tracked_changes_that_conflict_append(self): gl.branch(c='tmp', divergent_point='HEAD~1') gl.switch('tmp') utils.append_to_file(self.MASTER_FILE, contents='uncommitted') self.assertRaisesRegexp( ErrorReturnCode, 'failed to apply', gl.fuse, 'master', insertion_point='HEAD') contents = utils.read_file(self.MASTER_FILE) self.assertTrue('uncommitted' in contents) self.assertTrue('contents 2' in contents) # def test_uncommitted_untracked_changes_that_conflict(self): # utils.write_file(self.OTHER_FILE, contents='uncommitted in master') # try: # gl.fuse(self.OTHER) # self.fail() # except ErrorReturnCode as e: # self.assertTrue('failed to apply' in utils.stderr(e)) class TestMerge(TestOp): def test_uncommitted_changes(self): utils.write_file(self.MASTER_FILE, contents='uncommitted') utils.write_file('master_untracked', contents='uncommitted') gl.merge(self.OTHER) self.assertEqual('uncommitted', utils.read_file(self.MASTER_FILE)) self.assertEqual('uncommitted', utils.read_file('master_untracked')) def test_uncommitted_tracked_changes_that_conflict(self): gl.branch(c='tmp', divergent_point='HEAD~1') gl.switch('tmp') utils.write_file(self.MASTER_FILE, contents='uncommitted') self.assertRaisesRegexp( ErrorReturnCode, 'failed to apply', gl.merge, 'master') contents = utils.read_file(self.MASTER_FILE) self.assertTrue('uncommitted' in contents) self.assertTrue('contents 2' in contents) def test_uncommitted_tracked_changes_that_conflict_append(self): gl.branch(c='tmp', divergent_point='HEAD~1') gl.switch('tmp') utils.append_to_file(self.MASTER_FILE, contents='uncommitted') self.assertRaisesRegexp( ErrorReturnCode, 'failed to apply', gl.merge, 'master') contents = utils.read_file(self.MASTER_FILE) self.assertTrue('uncommitted' in contents) self.assertTrue('contents 2' in contents) class TestPerformance(TestEndToEnd): FPS_QTY = 10000 def setUp(self): super(TestPerformance, self).setUp() for i in range(0, self.FPS_QTY): fp = 'f' + text(i) utils.write_file(fp, fp) def test_status_performance(self): def assert_status_performance(): # The test fails if `gl status` takes more than 100 times # the time `git status` took. MAX_TOLERANCE = 100 t = time.time() gl.status() gl_t = time.time() - t t = time.time() git.status() git_t = time.time() - t self.assertTrue( gl_t < git_t*MAX_TOLERANCE, msg='gl_t {0}, git_t {1}'.format(gl_t, git_t)) # All files are untracked assert_status_performance() # Track all files, repeat logging.info('Doing a massive git add, this might take a while') git.add('.') logging.info('Done') assert_status_performance() def test_branch_switch_performance(self): MAX_TOLERANCE = 100 gl.commit('f1', m='commit') t = time.time() gl.branch(c='develop') gl.switch('develop') gl_t = time.time() - t # go back to previous state gl.switch('master') # do the same for git t = time.time() git.branch('gitdev') git.stash.save('--all') git.checkout('gitdev') git_t = time.time() - t self.assertTrue( gl_t < git_t*MAX_TOLERANCE, msg='gl_t {0}, git_t {1}'.format(gl_t, git_t)) gitless-0.8.8/gitless/tests/utils.py000066400000000000000000000063211347331605400175370ustar00rootroot00000000000000# -*- coding: utf-8 -*- # Gitless - a version control system built on top of Git # Licensed under MIT """Utility library for tests.""" from __future__ import unicode_literals import io from locale import getpreferredencoding import logging import os import re import shutil import stat import sys import tempfile import unittest if sys.platform != 'win32': from sh import git, ErrorReturnCode else: from pbs import ErrorReturnCode, Command git = Command('git') IS_PY2 = sys.version_info[0] == 2 ENCODING = getpreferredencoding() or 'utf-8' class TestBase(unittest.TestCase): def setUp(self, prefix_for_tmp_repo): """Creates temporary dir and cds to it.""" logging.basicConfig(stream=sys.stdout, level=logging.INFO) self.path = tempfile.mkdtemp(prefix=prefix_for_tmp_repo) logging.debug('Created temporary directory {0}'.format(self.path)) os.chdir(self.path) def tearDown(self): """Removes the temporary dir.""" rmtree(self.path) # Python 2/3 compatibility def assertItemsEqual(self, actual, expected, msg=None): try: return super(TestBase, self).assertItemsEqual(actual, expected, msg=msg) except AttributeError: try: # Checks that actual and expected have the same elements in the same # number, regardless of their order return super(TestBase, self).assertCountEqual(actual, expected, msg=msg) except AttributeError: return self.assertEqual(sorted(actual), sorted(expected), msg=msg) def assertRaisesRegexp(self, exc, r, fun, *args, **kwargs): try: fun(*args, **kwargs) self.fail('Exception not raised') except exc as e: msg = stderr(e) if isinstance(e, ErrorReturnCode) else str(e) if not re.search(r, msg): self.fail('No "{0}" found in "{1}"'.format(r, msg)) def rmtree(path): # On Windows, running shutil.rmtree on a folder that contains read-only # files throws errors. To workaround this, if removing a path fails, we make # the path writable and then try again def onerror(func, path, unused_exc_info): # error handler for rmtree if not os.access(path, os.W_OK): os.chmod(path, stat.S_IWUSR) func(path) else: # Swallow errors for now (on Windows there seems to be something weird # going on and we can't remove the temp directory even after all files # in it have been successfully removed) pass shutil.rmtree(path, onerror=onerror) logging.debug('Removed dir {0}'.format(path)) def write_file(fp, contents=''): _x_file('w', fp, contents=contents) def append_to_file(fp, contents=''): _x_file('a', fp, contents=contents) def set_test_config(): git.config('user.name', 'test') git.config('user.email', 'test@test.com') def read_file(fp): with io.open(fp, mode='r', encoding=ENCODING) as f: ret = f.read() return ret def stdout(p): return p.stdout.decode(ENCODING) def stderr(p): return p.stderr.decode(ENCODING) # Private functions def _x_file(x, fp, contents=''): assert not IS_PY2 or isinstance(contents, unicode) if not contents: contents = fp dirs, _ = os.path.split(fp) if dirs and not os.path.exists(dirs): os.makedirs(dirs) with io.open(fp, mode=x, encoding=ENCODING) as f: f.write(contents) gitless-0.8.8/gl.py000077500000000000000000000003671347331605400141740ustar00rootroot00000000000000#!/usr/bin/env python # -*- coding: utf-8 -*- # Gitless - a version control system built on top of Git # Licensed under MIT # This file is for PyInstaller import sys from gitless.cli import gl if __name__ == '__main__': sys.exit(gl.main()) gitless-0.8.8/gl.spec000066400000000000000000000011541347331605400144660ustar00rootroot00000000000000# -*- mode: python -*- import os a = Analysis(['gl.py'], pathex=[os.getcwd()], hiddenimports=[ # https://github.com/pyinstaller/pyinstaller/issues/3198 # remove this when dropping support for Python < 3.7 '_sysconfigdata', '_cffi_backend'], hookspath=None, runtime_hooks=None) pyz = PYZ(a.pure) exe = EXE(pyz, a.scripts, a.binaries, a.zipfiles, a.datas, name='gl', debug=False, strip=None, upx=True, console=True ) gitless-0.8.8/requirements.txt000066400000000000000000000002301347331605400164660ustar00rootroot00000000000000# make sure to update setup.py pygit2==0.28.2 # requires libgit2 0.28 clint==0.5.1 sh==1.12.14;sys_platform!='win32' pbs==0.110;sys_platform=='win32' gitless-0.8.8/setup.py000077500000000000000000000051711347331605400147300ustar00rootroot00000000000000#!/usr/bin/env python # -*- coding: utf-8 -*- import ast import re import sys from setuptools import setup _version_re = re.compile(r'__version__\s+=\s+(.*)') with open('gitless/cli/gl.py', 'rb') as f: version = str(ast.literal_eval(_version_re.search( f.read().decode('utf-8')).group(1))) # Build helper if sys.argv[-1] == 'gl-build': from sh import pyinstaller import shutil import tarfile import platform rel = 'gl-v{0}-{1}-{2}'.format( version, platform.system().lower(), platform.machine()) print('running pyinstaller...') pyinstaller( 'gl.spec', clean=True, distpath=rel, _out=sys.stdout, _err=sys.stderr) print('success!! gl binary should be at {0}/gl'.format(rel)) print('creating tar.gz file') shutil.copy('README.md', rel) shutil.copy('LICENSE.md', rel) with tarfile.open(rel + '.tar.gz', 'w:gz') as tar: tar.add(rel) print('success!! binary release at {0}'.format(rel + '.tar.gz')) sys.exit() ld = """ Gitless is an experimental version control system built on top of Git. Many people complain that Git is hard to use. We think the problem lies deeper than the user interface, in the concepts underlying Git. Gitless is an experiment to see what happens if you put a simple veneer on an app that changes the underlying concepts. Because Gitless is implemented on top of Git (could be considered what Git pros call a \"porcelain\" of Git), you can always fall back on Git. And of course your coworkers you share a repo with need never know that you're not a Git aficionado. More info, downloads and documentation @ `Gitless's website `__. """ setup( name='gitless', version=version, description='A version control system built on top of Git', long_description=ld, author='Santiago Perez De Rosso', author_email='sperezde@csail.mit.edu', url='http://gitless.com', packages=['gitless', 'gitless.cli'], install_requires=[ # make sure it matches requirements.txt 'pygit2==0.28.2', # requires libgit2 0.28 'clint>=0.3.6', 'sh>=1.11' if sys.platform != 'win32' else 'pbs>=0.11' ], license='MIT', classifiers=[ 'Development Status :: 2 - Pre-Alpha', 'Intended Audience :: Developers', 'License :: OSI Approved :: MIT License', 'Natural Language :: English', 'Programming Language :: Python', 'Programming Language :: Python :: 2', 'Programming Language :: Python :: 3', 'Topic :: Software Development :: Version Control'], entry_points={ 'console_scripts': [ 'gl = gitless.cli.gl:main' ]}, test_suite='gitless.tests') gitless-0.8.8/snap/000077500000000000000000000000001347331605400141505ustar00rootroot00000000000000gitless-0.8.8/snap/snapcraft.yaml000066400000000000000000000024311347331605400170150ustar00rootroot00000000000000# obligatory fields name: gitless version: git summary: A simple version control system built on top of Git description: | Gitless is an experimental version control system built on top of Git. Many people complain that Git is hard to use. We think the problem lies deeper than the user interface, in the concepts underlying Git. Gitless is an experiment to see what happens if you put a simple veneer on an app that changes the underlying concepts. Because Gitless is implemented on top of Git (could be considered what Git pros call a "porcelain" of Git), you can always fall back on Git. And of course your coworkers you share a repo with need never know that you're not a Git aficionado. base: core18 grade: devel # 'stable' for stable/candidate upload confinement: devmode # 'strict' after right plugs and slots # 'optional' fields apps: gl: command: bin/gl parts: libgit2: plugin: cmake # https://www.pygit2.org/install.html#version-numbers source: https://github.com/libgit2/libgit2/archive/v0.28.2.tar.gz build-packages: - libssl-dev gitless-cli: plugin: python source: . after: [libgit2] # need git until https://github.com/sdg-mit/gitless/issues/176 stage-packages: - git build-packages: - git gitless-0.8.8/tox.ini000066400000000000000000000001531347331605400145210ustar00rootroot00000000000000[tox] envlist = py27, py34 [testenv] deps=nose commands= nosetests nosetests gitless/tests/test_e2e.py