diff --git a/.travis.sh b/.travis.sh index 526e541..f7c041d 100755 --- a/.travis.sh +++ b/.travis.sh @@ -2,7 +2,7 @@ cd ~ -git clone --depth=1 -b maint/v0.22 https://github.com/libgit2/libgit2.git +git clone --depth=1 -b maint/v0.23 https://github.com/libgit2/libgit2.git cd libgit2/ mkdir build && cd build diff --git a/.travis.yml b/.travis.yml index df89aca..1a024be 100644 --- a/.travis.yml +++ b/.travis.yml @@ -19,11 +19,13 @@ before_script: - git config --global user.email "travis@test.com" script: - - nosetests + - nosetests --logging-level=WARN # nose doesn't like the number on test_e2e so it's not detected by the # previous command. - - nosetests gitless/tests/test_e2e.py + - nosetests gitless/tests/test_e2e.py --logging-level=WARN branches: only: - develop + +sudo: false diff --git a/README.md b/README.md index ce2caa3..9467749 100644 --- a/README.md +++ b/README.md @@ -63,7 +63,7 @@ the Python Package Index). To install from source you need to have Python (2.6, 2.7, 3.2+ or pypy) installed. -Additionaly, you need to [install pygit2]( +Additionaly, you need to [install pygit2 (v0.23.0)]( http://www.pygit2.org/install.html "pygit2 install"). Then, download the source code tarball available @ @@ -78,7 +78,7 @@ 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.6, 2.7, 3.2+ or pypy) installed. -Additionaly, you need to [install pygit2]( +Additionaly, you need to [install pygit2 (v0.23.0)]( http://www.pygit2.org/install.html "pygit2 install"). Then, just do: diff --git a/gitless/cli/commit_dialog.py b/gitless/cli/commit_dialog.py index b2b0bcf..51af52d 100644 --- a/gitless/cli/commit_dialog.py +++ b/gitless/cli/commit_dialog.py @@ -20,7 +20,7 @@ IS_PY2 = sys.version_info[0] == 2 ENCODING = getpreferredencoding() or 'utf-8' -_COMMIT_FILE = '.GL_COMMIT_EDIT_MSG' +_COMMIT_FILE = 'GL_COMMIT_EDIT_MSG' _MERGE_MSG_FILE = 'MERGE_MSG' diff --git a/gitless/cli/file_cmd.py b/gitless/cli/file_cmd.py index 23c199e..8d56741 100644 --- a/gitless/cli/file_cmd.py +++ b/gitless/cli/file_cmd.py @@ -15,7 +15,8 @@ def parser(help_msg, subcmd): def f(subparsers, repo): - p = subparsers.add_parser(subcmd, help=help_msg) + 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) diff --git a/gitless/cli/gl.py b/gitless/cli/gl.py index 6bfa550..823c789 100644 --- a/gitless/cli/gl.py +++ b/gitless/cli/gl.py @@ -29,7 +29,7 @@ INTERNAL_ERROR = 3 NOT_IN_GL_REPO = 4 -VERSION = '0.8' +VERSION = '0.8.1' URL = 'http://gitless.com' @@ -51,7 +51,7 @@ def main(): '--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(dest='subcmd_name') + subparsers = parser.add_subparsers(title='subcommands', dest='subcmd_name') subparsers.required = True sub_cmds = [ diff --git a/gitless/cli/gl_branch.py b/gitless/cli/gl_branch.py index f0200cf..155fa70 100644 --- a/gitless/cli/gl_branch.py +++ b/gitless/cli/gl_branch.py @@ -7,6 +7,11 @@ from __future__ import unicode_literals +try: + from StringIO import StringIO +except ImportError: + from io import StringIO + from clint.textui import colored from gitless import core @@ -16,31 +21,41 @@ def parser(subparsers, _): """Adds the branch parser to the given subparsers object.""" + desc = 'list, create, edit or delete branches' branch_parser = subparsers.add_parser( - 'branch', help='list, create, edit or delete branches') + 'branch', help=desc, description=desc.capitalize()) branch_parser.add_argument( '-r', '--remote', help='list remote branches in addition to local branches', action='store_true') branch_parser.add_argument( - '-c', '--create', nargs='+', help='create branch(es)', dest='create_b') + '-c', '--create', nargs='+', help='create branch(es)', dest='create_b', + metavar='branch') branch_parser.add_argument( '-dp', '--divergent-point', help='the commit from where to \'branch out\' (only relevant if a new ' 'branch is created; defaults to HEAD)', default='HEAD', dest='dp') branch_parser.add_argument( - '-d', '--delete', nargs='+', help='delete branch(es)', dest='delete_b') + '-d', '--delete', nargs='+', help='delete branch(es)', dest='delete_b', + metavar='branch') + branch_parser.add_argument( + '-sh', '--set-head', help='set the head of the current branch', + dest='new_head', metavar='commit_id') branch_parser.add_argument( '-su', '--set-upstream', help='set the upstream branch of the current branch', - dest='upstream_b') + dest='upstream_b', metavar='branch') branch_parser.add_argument( '-uu', '--unset-upstream', help='unset the upstream branch of the current branch', action='store_true') + + branch_parser.add_argument( + '-v', '--verbose', help='be verbose, will output the head of each branch', + action='store_true') branch_parser.set_defaults(func=main) @@ -54,35 +69,38 @@ def main(args, repo): 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) + _do_list(repo, args.remote, v=args.verbose) return ret -def _do_list(repo, list_remote): +def _do_list(repo, list_remote, v=False): pprint.msg('List of branches:') - pprint.exp('do gl branch -c to create branch b') - pprint.exp('do gl branch -d to delete branch b') - pprint.exp('do gl switch to switch to branch b') + 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 repo.listall_branches()): current_str = '*' if b.is_current else ' ' - upstream_str = '' - try: - upstream_str = '(upstream is {0})'.format(b.upstream_name) - except KeyError: - pass + 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(_ci_str(b.head))) if list_remote: for r in repo.remotes: for b in (r.lookup_branch(n) for n in r.listall_branches()): pprint.item(' {0}'.format(colored.yellow(b.branch_name))) + if v: + pprint.item(' ➜ head is {0}'.format(_ci_str(b.head))) def _do_create(create_b, dp, repo): @@ -91,7 +109,7 @@ def _do_create(create_b, dp, repo): try: target = repo.revparse_single(dp) except KeyError: - raise ValueError('Invalid divergent point "{0}"'.format(dp)) + raise ValueError('Invalid divergent point {0}'.format(dp)) for b_name in create_b: r = repo @@ -142,7 +160,7 @@ def _do_delete(delete_b, repo): except core.BranchIsCurrentError as e: pprint.err(e) pprint.err_exp( - 'do gl branch to create or switch to another branch b and then ' + '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 @@ -161,3 +179,22 @@ def _do_unset_upstream(repo): 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, _ci_str(commit))) + return True + + +def _ci_str(ci): + ci_str = StringIO() + pprint.commit(ci, compact=True, stream=ci_str.write) + return ci_str.getvalue().strip() diff --git a/gitless/cli/gl_checkout.py b/gitless/cli/gl_checkout.py index 8062750..c4b6d5d 100644 --- a/gitless/cli/gl_checkout.py +++ b/gitless/cli/gl_checkout.py @@ -14,8 +14,9 @@ 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='checkout committed versions of files') + '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.'), diff --git a/gitless/cli/gl_commit.py b/gitless/cli/gl_commit.py index 1780942..9ce34fb 100644 --- a/gitless/cli/gl_commit.py +++ b/gitless/cli/gl_commit.py @@ -15,12 +15,13 @@ 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='record changes in the local repository', - description=( + 'commit', help=desc, description=( + desc.capitalize() + '. ' + 'By default all tracked modified files are committed. To customize the' - ' set of files to be committed you can use the only, exclude, and ' - 'include flags')) + ' set of files to be committed use the only, exclude, and include ' + 'flags')) commit_parser.add_argument( '-m', '--message', help='Commit message', dest='m') helpers.oei_flags(commit_parser, repo) @@ -32,7 +33,7 @@ def main(args, repo): if not commit_files: pprint.err('No files to commit') - pprint.err_exp('use gl track if you want to track changes to file f') + pprint.err_exp('use gl track f if you want to track changes to file f') return False msg = args.m if args.m else commit_dialog.show(commit_files, repo) @@ -48,13 +49,9 @@ def main(args, repo): pprint.commit(ci) if curr_b.fuse_in_progress: - pprint.blank() - try: - curr_b.fuse_continue(fuse_cb=pprint.FUSE_CB) - pprint.ok('Fuse succeeded') - except core.ApplyFailedError as e: - pprint.ok('Fuse succeeded') - raise e + _op_continue(curr_b.fuse_continue, 'Fuse') + elif curr_b.merge_in_progress: + _op_continue(curr_b.merge_continue, 'Merge') return True @@ -65,3 +62,13 @@ def _auto_track(files, curr_b): 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(op)) + except core.ApplyFailedError as e: + pprint.ok('{0} succeeded'.format(op)) + raise e diff --git a/gitless/cli/gl_diff.py b/gitless/cli/gl_diff.py index 4b525fb..dee799d 100644 --- a/gitless/cli/gl_diff.py +++ b/gitless/cli/gl_diff.py @@ -15,12 +15,12 @@ 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='show changes in files', - description=( + 'diff', help=desc, description=( + desc.capitalize() + '. ' + 'By default all tracked modified files are diffed. To customize the ' - ' set of files to diff you can use the only, exclude, and include ' - 'flags')) + ' set of files to diff use the only, exclude, and include flags')) helpers.oei_flags(diff_parser, repo) diff_parser.set_defaults(func=main) @@ -54,7 +54,7 @@ def main(args, repo): pprint.diff(patch, stream=tf.write) if os.path.getsize(tf.name) > 0: - helpers.page(tf.name) + helpers.page(tf.name, repo) os.remove(tf.name) return success diff --git a/gitless/cli/gl_fuse.py b/gitless/cli/gl_fuse.py index e16021c..8d9b60d 100644 --- a/gitless/cli/gl_fuse.py +++ b/gitless/cli/gl_fuse.py @@ -13,13 +13,13 @@ def parser(subparsers, repo): + desc = 'fuse the divergent changes of a branch onto the current branch' fuse_parser = subparsers.add_parser( - 'fuse', - help='fuse the divergent changes of a branch onto the current branch', - description=( + 'fuse', help=desc, description=( + desc.capitalize() + '. ' + 'By default all divergent changes from the given source branch are ' - 'fused. To customize the set of commmits to be fused you can use the ' - 'only and exclude flags')) + 'fused. To customize the set of commmits to fuse use the only and ' + 'exclude flags')) fuse_parser.add_argument( 'src', nargs='?', help=( @@ -50,7 +50,7 @@ def parser(subparsers, repo): def main(args, repo): current_b = repo.current_branch if args.abort: - current_b.abort_fuse(fuse_cb=pprint.FUSE_CB) + current_b.abort_fuse(op_cb=pprint.OP_CB) pprint.ok('Fuse aborted successfully') return True @@ -90,7 +90,7 @@ def valid_input(inp): try: current_b.fuse( src_branch, insertion_point, only=only, exclude=exclude, - fuse_cb=pprint.FUSE_CB) + op_cb=pprint.OP_CB) pprint.ok('Fuse succeeded') except core.ApplyFailedError as e: pprint.ok('Fuse succeeded') diff --git a/gitless/cli/gl_history.py b/gitless/cli/gl_history.py index 2379ad4..a38d048 100644 --- a/gitless/cli/gl_history.py +++ b/gitless/cli/gl_history.py @@ -10,37 +10,43 @@ import os import tempfile -from clint.textui import colored - 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='show commit history') + '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): - curr_b = repo.current_branch + b = helpers.get_branch(args.b, repo) if args.b else repo.current_branch with tempfile.NamedTemporaryFile(mode='w', delete=False) as tf: - for ci in curr_b.history(): - merge_commit = len(ci.parent_ids) > 1 - color = colored.magenta if merge_commit else colored.yellow - if merge_commit: - pprint.puts(color('Merge commit'), stream=tf.write) - merges_str = ' '.join(str(oid)[:7] for oid in ci.parent_ids) - pprint.puts(color('Merges: {0}'.format(merges_str)), stream=tf.write) - pprint.commit(ci, color=color, stream=tf.write) - pprint.puts(stream=tf.write) - pprint.puts(stream=tf.write) - if args.verbose and len(ci.parents) == 1: # TODO: merge commits diffs - for patch in curr_b.diff_commits(ci.parents[0], ci): + 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) - helpers.page(tf.name) + + count += 1 + helpers.page(tf.name, repo) os.remove(tf.name) return True diff --git a/gitless/cli/gl_init.py b/gitless/cli/gl_init.py index 0e9ec2f..0152007 100644 --- a/gitless/cli/gl_init.py +++ b/gitless/cli/gl_init.py @@ -16,11 +16,11 @@ def parser(subparsers, _): """Adds the init parser to the given subparsers object.""" + desc = ( + 'create an empty Gitless\'s repository or create one from an existing ' + 'remote repository') init_parser = subparsers.add_parser( - 'init', - help=( - 'create an empty Gitless\'s repository or create one from an ' - 'existing remote repository')) + 'init', help=desc, description=desc.capitalize()) init_parser.add_argument( 'repo', nargs='?', help=( diff --git a/gitless/cli/gl_merge.py b/gitless/cli/gl_merge.py index 91e3036..cebcb21 100644 --- a/gitless/cli/gl_merge.py +++ b/gitless/cli/gl_merge.py @@ -7,13 +7,15 @@ 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='merge the divergent changes of one branch onto another') + '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') @@ -29,6 +31,11 @@ def main(args, repo): pprint.ok('Merge aborted successfully') return True - current_b.merge(helpers.get_branch_or_use_upstream(args.src, 'src', repo)) - pprint.ok('Merge succeeded') + 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 diff --git a/gitless/cli/gl_publish.py b/gitless/cli/gl_publish.py index 6a305cd..b9bb6cc 100644 --- a/gitless/cli/gl_publish.py +++ b/gitless/cli/gl_publish.py @@ -12,8 +12,9 @@ def parser(subparsers, _): """Adds the publish parser to the given subparsers object.""" + desc = 'publish commits upstream' publish_parser = subparsers.add_parser( - 'publish', help='publish commits upstream') + '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) diff --git a/gitless/cli/gl_remote.py b/gitless/cli/gl_remote.py index bc4806a..b9d7e46 100644 --- a/gitless/cli/gl_remote.py +++ b/gitless/cli/gl_remote.py @@ -12,15 +12,18 @@ 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='list, create, edit or delete remotes') + 'remote', help=desc, description=desc.capitalize()) remote_parser.add_argument( - '-c', '--create', nargs='?', help='create remote', dest='remote_name') + '-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') + '-d', '--delete', nargs='+', help='delete remote(es)', dest='delete_r', + metavar='remote') remote_parser.set_defaults(func=main) @@ -42,8 +45,8 @@ def main(args, repo): def _do_list(remotes): pprint.msg('List of remotes:') pprint.exp( - 'do gl remote -c to add a new remote r mapping to r_url') - pprint.exp('do gl remote -d to delete remote r') + '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): diff --git a/gitless/cli/gl_status.py b/gitless/cli/gl_status.py index e4589e6..c5a6158 100644 --- a/gitless/cli/gl_status.py +++ b/gitless/cli/gl_status.py @@ -18,8 +18,9 @@ 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='show status of the repo') + 'status', help=desc, description=desc.capitalize()) status_parser.add_argument( 'paths', nargs='*', help='the specific path(s) to status', action=helpers.PathProcessor, repo=repo) @@ -69,9 +70,9 @@ 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 if you don\'t want to track changes to file f') + '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 to discard ' + 'if file f was committed before, use gl checkout f to discard ' 'local changes') pprint.blank() @@ -103,7 +104,7 @@ def _print_tracked_mod_files(tracked_mod_list, relative_paths, repo): 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 if you want to track changes to file f') + pprint.exp('use gl track f if you want to track changes to file f') pprint.blank() if not untracked_list: @@ -112,20 +113,23 @@ def _print_untracked_files(untracked_list, relative_paths, repo): root = repo.root for f in untracked_list: - s = '' + exp = '' color = colored.blue - if f.exists_at_head: + if f.in_conflict: + exp = ' (with conflicts)' + color = colored.cyan + elif f.exists_at_head: color = colored.magenta if f.exists_in_wd: - s = ' (exists at head)' + exp = ' (exists at head)' else: - s = ' (exists at head but not in working directory)' + 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=s) + pprint.item(color(fp), opt_text=exp) def _print_conflict_exp(op): @@ -134,6 +138,6 @@ def _print_conflict_exp(op): '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 to mark file f as resolved') + 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() diff --git a/gitless/cli/gl_switch.py b/gitless/cli/gl_switch.py index 6f2eef0..d61a9f5 100644 --- a/gitless/cli/gl_switch.py +++ b/gitless/cli/gl_switch.py @@ -12,8 +12,9 @@ def parser(subparsers, _): """Adds the switch parser to the given subparsers object.""" + desc = 'switch branches' switch_parser = subparsers.add_parser( - 'switch', help='switch branches') + 'switch', help=desc, description=desc.capitalize()) switch_parser.add_argument('branch', help='switch to branch') switch_parser.add_argument( '-mo', '--move-over', diff --git a/gitless/cli/helpers.py b/gitless/cli/helpers.py index 827c14d..c2c01f9 100644 --- a/gitless/cli/helpers.py +++ b/gitless/cli/helpers.py @@ -56,8 +56,14 @@ def get_branch_or_use_upstream(branch_name, arg, repo): return ret -def page(fp): - subprocess.call(['less', '-r', '-f', fp], stdin=sys.stdin, stdout=sys.stdout) +def page(fp, repo): + pager = '' + try: + pager = repo.config['core.pager'] + except KeyError: + pass + cmd = [pager, fp] if pager else ['less', '-r', '-f', fp] + subprocess.call(cmd, stdin=sys.stdin, stdout=sys.stdout) class PathProcessor(argparse.Action): diff --git a/gitless/cli/pprint.py b/gitless/cli/pprint.py index 8a18b6a..a42cd44 100644 --- a/gitless/cli/pprint.py +++ b/gitless/cli/pprint.py @@ -124,8 +124,17 @@ def get_user_input(text='> '): return input(text) -def commit(ci, color=colored.yellow, stream=sys.stdout.write): +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) @@ -136,7 +145,7 @@ def commit(ci, color=colored.yellow, stream=sys.stdout.write): with indent(4): puts(ci.message, stream=stream) -# Fuse Callbacks +# Op Callbacks def apply_ok(ci): ok('Insertion of {0} succeeded'.format(ci.id)) @@ -151,12 +160,12 @@ def apply_err(ci): blank() def save(): - warn('Uncommitted changes would prevent fuse, temporarily saving them') + warn('Temporarily saving uncommitted changes') def restore_ok(): ok('Uncommitted changes applied successfully to the new head of the branch') -FUSE_CB = core.FuseCb(apply_ok, apply_err, save, restore_ok) +OP_CB = core.OpCb(apply_ok, apply_err, save, restore_ok) class FixedOffset(tzinfo): diff --git a/gitless/core.py b/gitless/core.py index 9b388da..910f59c 100644 --- a/gitless/core.py +++ b/gitless/core.py @@ -15,6 +15,7 @@ pass import itertools +import json from locale import getpreferredencoding import os import re @@ -51,9 +52,6 @@ def init_repository(url=None): Args: url: if given the local repository will be a clone of the remote repository given by this url. - - Returns: - the Gitless's repository created """ cwd = os.getcwd() try: @@ -96,11 +94,7 @@ class Repository(object): """ def __init__(self): - """Create a Repository out of the current working repository. - - Raises: - NotInRepoError: if there's no current working repository. - """ + """Create a Repository out of the current working repository.""" try: path = pygit2.discover_repository(os.getcwd()) except KeyError: @@ -135,9 +129,31 @@ def merge_base(self, b1, b2): except KeyError: raise GlError('No common commit found between {0} and {1}'.format(b1, b2)) - @property - def _fuse_commits_fp(self): - return os.path.join(self.path, 'gl_fuse_commits') + 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 # Branch related methods @@ -145,25 +161,15 @@ def _fuse_commits_fp(self): @property def current_branch(self): if self.git_repo.head_is_detached: - if os.path.exists(self._fuse_commits_fp): # fuse in progress - b_name = self.git_repo.lookup_reference('ORIG_HEAD').resolve().shorthand - else: - raise Exception('Gl confused') + b = self.git_repo.lookup_reference('GL_FUSE_ORIG_HEAD').resolve() else: - b_name = self.git_repo.head.shorthand - return self.lookup_branch(b_name) - - - def create_branch(self, name, commit): - """Create a new branch. + b = self.git_repo.head + return self.lookup_branch(b.shorthand) - Args: - name: the name of the new branch. - commit: the commit that is to become the "head" of the new branch. - """ + def create_branch(self, name, head): try: return Branch( - self.git_repo.create_branch(name, commit, False), # force=False + 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 @@ -172,7 +178,6 @@ def create_branch(self, name, commit): str(e).replace('refs/heads/', '').replace('reference', 'branch')) def lookup_branch(self, branch_name): - """Return the branch object corresponding to the given branch name.""" git_branch = self.git_repo.lookup_branch( branch_name, pygit2.GIT_BRANCH_LOCAL) if git_branch: @@ -186,71 +191,163 @@ def listall_branches(self): """ return self.git_repo.listall_branches(pygit2.GIT_BRANCH_LOCAL) - def switch_current_branch(self, b, move_over=False): + def switch_current_branch(self, dst_b, move_over=False): """Switches to the given branch. Args: - b: the destination branch. + dst_b: the destination branch. move_over: if True, then uncommited changes made in the current branch are moved to the destination branch (defaults to False). """ - if b.is_current: + if dst_b.is_current: raise ValueError( 'You are already on branch {0}. No need to switch.'.format( - b.branch_name)) - - curr_b = self.current_branch - - # TODO: let the user switch even if a fuse or merge is in progress - curr_b._check_op_not_in_progress() - - # Helper functions for switch - - def au_fp(branch): - return os.path.join( - self.path, 'GL_AU_{0}'.format(branch.branch_name.replace('/', '_'))) - - def unmark_au_files(): - """Saves the filepaths marked as assumed unchanged and unmarks them.""" - assumed_unchanged_fps = curr_b._au_files() - if not assumed_unchanged_fps: - return - - with io.open(au_fp(curr_b), mode='w', encoding=ENCODING) as f: - for fp in assumed_unchanged_fps: - f.write(fp + '\n') - git('update-index', '--no-assume-unchanged', fp, - _cwd=self.root) - - def remark_au_files(): - """Re-marks files as assumed unchanged.""" - au = au_fp(b) - if not os.path.exists(au): + 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 - with io.open(au, mode='r', encoding=ENCODING) as f: - for fp in f: - git('update-index', '--assume-unchanged', fp.strip(), - _cwd=self.root) - - os.remove(au) - - - # Stash doesn't save assumed unchanged files, so we save which files are - # marked as assumed unchanged and unmark them. And when switching back we - # look at this info and re-mark them. - - unmark_au_files() - if not move_over: - git.stash.save('--all', '--', _stash_msg(curr_b.branch_name)) + 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) - self.git_repo.checkout(b.git_branch) + split_msg = msg.split(INFO_SEP) - s_id = _stash_id(_stash_msg(b.branch_name)) - if s_id: - git.stash.pop(s_id) + 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) - remark_au_files() + # 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): @@ -278,9 +375,9 @@ def __contains__(self, name): def create(self, name, url): if '/' in name: raise ValueError( - 'Invalid remote name \'{0}\': remotes can\'t have \'/\''.format(name)) + 'Invalid remote name {0}: remotes can\'t have \'/\''.format(name)) if not url.strip(): - raise ValueError('Invalid url \'{0}\''.format(url)) + raise ValueError('Invalid url {0}'.format(url)) # Check that the given url corresponds to a git repo try: @@ -308,14 +405,7 @@ def __init__(self, git_remote, gl_repo): self.name = self.git_remote.name self.url = self.git_remote.url - - def create_branch(self, name, commit): - """Create a new branch in the remote repository. - - Args: - name: the name of the new branch. - commit: the commit that is to become the "head" of the new branch. - """ + def create_branch(self, name, head): if self.lookup_branch(name): raise GlError( 'Branch {0} already exists in remote repository {1}'.format( @@ -323,7 +413,7 @@ def create_branch(self, name, commit): # 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', commit) + 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) @@ -332,7 +422,6 @@ def create_branch(self, name, commit): finally: tmp_b.delete() - def listall_branches(self): """Return a list with the names of all the branches in this repository. @@ -344,7 +433,6 @@ def listall_branches(self): yield regex.match(head).group(1) def lookup_branch(self, branch_name): - """Return the RemoteBranch object corresponding to the given branch name.""" if not stdout(git('ls-remote', '--heads', self.name, branch_name)): return None # The branch exists in the remote @@ -370,7 +458,6 @@ def __init__(self, git_branch, 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)) @@ -418,47 +505,42 @@ def __init__(self, git_branch, gl_repo): 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') - branch_name = self.branch_name self.git_branch.delete() # We also cleanup any stash left - s_id = _stash_id(_stash_msg(branch_name)) + s_id, _ = _stash(_stash_msg(self.branch_name)) if s_id: git.stash.drop(s_id) @property def upstream(self): - if not self.git_branch.upstream: + git_upstream = self.git_branch.upstream + if not git_upstream: return None try: - self.git_branch.upstream.remote_name - except ValueError: - # It is a local branch - return Branch(self.git_branch.upstream, self.gl_repo) - return RemoteBranch(self.git_branch.upstream, self.gl_repo) + 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 upstream_name(self): - upstream = self.git_branch.upstream - if upstream: - return upstream.branch_name - raise KeyError('Branch has no upstream set') - @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.""" @@ -505,35 +587,37 @@ def __getattr__(self, name): return Index(self.gl_repo.git_repo.index) _st_map = { - # git status: gl status, exists_at_head, exists_in_wd, modified + # git status: gl status, exists_at_head, exists_in_wd, modified, conflict - pygit2.GIT_STATUS_CURRENT: (GL_STATUS_TRACKED, True, True, False), - pygit2.GIT_STATUS_IGNORED: (GL_STATUS_IGNORED, False, True, True), + 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), - pygit2.GIT_STATUS_WT_MODIFIED: (GL_STATUS_TRACKED, True, True, True), - pygit2.GIT_STATUS_WT_DELETED: (GL_STATUS_TRACKED, True, False, True), + 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), - pygit2.GIT_STATUS_INDEX_MODIFIED: (GL_STATUS_TRACKED, True, True, True), - pygit2.GIT_STATUS_INDEX_DELETED: (GL_STATUS_TRACKED, True, False, True), + 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). Also, for some reason, - # files with conflicts have this status code + # `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), + 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), + GL_STATUS_TRACKED, False, True, True, False), pygit2.GIT_STATUS_WT_MODIFIED | pygit2.GIT_STATUS_INDEX_MODIFIED: ( - GL_STATUS_TRACKED, True, True, True), + GL_STATUS_TRACKED, True, True, True, False), # WT_MODIFIED | INDEX_DELETED -> can't happen ### WT_DELETED | INDEX_* ### -> can't happen @@ -556,17 +640,8 @@ def status(self): Ignored and tracked unmodified files are not reported. File paths are always relative to the repo root. """ - index = self._index - for fp, git_s in self.gl_repo.git_repo.status().items(): - in_conflict = False - if index.conflicts: - try: # `fp in index.conflicts` doesn't work - index.conflicts[fp] - in_conflict = True - except KeyError: - pass - yield self.FileStatus(fp, *(self._st_map[git_s] + (in_conflict,))) + yield self.FileStatus(fp, *self._st_map[git_s]) # status doesn't report au files au_files = self._au_files() @@ -583,27 +658,18 @@ def status_file(self, path): def _status_file(self, path): assert not os.path.isabs(path) - git_s = self.gl_repo.git_repo.status_file(path) - - cmd_out = stdout(git( - 'ls-files', '-v', '--full-name', path, _cwd=self.gl_repo.root)) - if cmd_out and cmd_out[0] == 'h': - exists_in_wd = os.path.exists(os.path.join(self.gl_repo.root, path)) - return ( - self.FileStatus( - path, GL_STATUS_UNTRACKED, True, exists_in_wd, True, False), - git_s, True) + git_st = self.gl_repo.git_repo.status_file(path) - index = self._index - in_conflict = False - if index.conflicts: - try: # `fp in index.conflicts` doesn't work - index.conflicts[path] - in_conflict = True - except KeyError: - pass - f_st = self.FileStatus(path, *(self._st_map[git_s] + (in_conflict,))) - return f_st, git_s, False + 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 # File related methods @@ -635,7 +701,7 @@ def track_file(self, path): raise GlError('File {0} in unkown status {1}'.format(path, git_st)) def untrack_file(self, path): - """Stop tracking changes to the given path.""" + """Stop tracking changes to path.""" assert not os.path.isabs(path) gl_st, git_st, is_au = self._status_file(path) @@ -688,7 +754,7 @@ def checkout_file(self, path, commit): index.add(path) def diff_file(self, path): - """Diff the working version of the given path with its committed version.""" + """Diff the working version of path with its committed version.""" assert not os.path.isabs(path) git_repo = self.gl_repo.git_repo @@ -710,7 +776,7 @@ def diff_file(self, path): # Merge related methods - def merge(self, src): + 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() @@ -719,17 +785,33 @@ def merge(self, src): if result & pygit2.GIT_MERGE_ANALYSIS_UP_TO_DATE: raise GlError('No commits to merge') try: - git.merge(src.branch_name, '--no-ff') + git.merge(src, '--no-ff') except ErrorReturnCode as e: - raise GlError(stdout(e) + stderr(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) + try: + git.merge(src, '--no-ff') + except ErrorReturnCode as e: + raise GlError(stdout(e) + stderr(e)) + + restore_fn = op_cb.restore_ok if op_cb else None + self._safe_restore(_stash_msg_merge, restore_fn=restore_fn) + self._state_cleanup() + + def merge_continue(self, op_cb=None): + if not self.merge_in_progress: + raise GlError('No merge in progress, nothing to continue') + restore_fn = op_cb.restore_ok if op_cb else None + self._safe_restore(_stash_msg_merge, restore_fn=restore_fn) + self._state_cleanup() @property def merge_in_progress(self): - try: - self.gl_repo.git_repo.lookup_reference('MERGE_HEAD') - return True - except KeyError: - return False + return self.gl_repo._ref_exists('MERGE_HEAD') def abort_merge(self): if not self.merge_in_progress: @@ -741,7 +823,7 @@ def abort_merge(self): @property def _fuse_commits_fp(self): - return self.gl_repo._fuse_commits_fp + return self.gl_repo._fuse_commits_fp(self) def _save_fuse_commits(self, commits): path = self._fuse_commits_fp @@ -755,7 +837,6 @@ def _save_fuse_commits(self, commits): 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: @@ -764,7 +845,7 @@ def _load_fuse_commits(self): yield git_repo[ci_id] os.remove(self._fuse_commits_fp) - def fuse(self, src, ip, only=None, exclude=None, fuse_cb=None): + def fuse(self, src, ip, only=None, exclude=None, op_cb=None): """Fuse the given commits onto this branch. Args: @@ -774,11 +855,12 @@ def fuse(self, src, ip, only=None, exclude=None, fuse_cb=None): divergent commits from self or the divergent point. only: ids of commits to use only. exclude: ids of commtis to exclude. - fuse_cb: see FuseCb. + 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 mb = repo.merge_base(self, src) @@ -810,42 +892,40 @@ def fuse(self, src, ip, only=None, exclude=None, fuse_cb=None): fuse_commits = itertools.chain([fuse_ci], fuse_commits) break detach_point = ci.id - if fuse_cb and fuse_cb.apply_ok: - fuse_cb.apply_ok(ci) - + 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, fuse_cb=fuse_cb) - self._safe_restore(fuse_cb=fuse_cb) + 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, 'ORIG_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, fuse_cb=fuse_cb) - - self._fuse(commits, fuse_cb=fuse_cb) + self._safe_reset(detach_point, _stash_msg_fuse, save_fn=save_fn) + self._fuse(commits, op_cb=op_cb) - def fuse_continue(self, fuse_cb=None): - """Resume a fuse in progress.""" + 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, fuse_cb=fuse_cb) + self._fuse(commits, op_cb=op_cb) - def _fuse(self, commits, fuse_cb=None): + def _fuse(self, commits, op_cb=None): git_repo = self.gl_repo.git_repo committer = git_repo.default_signature @@ -853,13 +933,13 @@ def _fuse(self, commits, fuse_cb=None): git_repo.cherrypick(ci.id) index = self._index if index.conflicts: - if fuse_cb and fuse_cb.apply_err: - fuse_cb.apply_err(ci) + 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 fuse_cb and fuse_cb.apply_ok: - fuse_cb.apply_ok(ci) + 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 @@ -867,30 +947,35 @@ def _fuse(self, commits, fuse_cb=None): [git_repo.head.target]) # We are done fusing => update original branch and re-attach head - orig_branch_ref = git_repo.lookup_reference('ORIG_HEAD').resolve() + 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) - git_repo.state_cleanup() - self._safe_restore(fuse_cb=fuse_cb) + 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): - # We could check if ORIG_HEAD exists but lots of git commands use ORIG_HEAD - # so we might get confused and think that we are in a fuse when we are not. - return os.path.exists(self._fuse_commits_fp) + return self.gl_repo._ref_exists('GL_FUSE_ORIG_HEAD') - def abort_fuse(self, fuse_cb=None): + 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('ORIG_HEAD').target) + 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) - os.remove(self._fuse_commits_fp) - git_repo.state_cleanup() - self._safe_restore(fuse_cb=fuse_cb) + 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, fuse_cb=None): + def _safe_reset(self, cid, msg_fn, save_fn=None): git_repo = self.gl_repo.git_repo tree = git_repo[cid].tree try: @@ -899,19 +984,19 @@ def _safe_reset(self, cid, fuse_cb=None): # 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 fuse_cb and fuse_cb.save: - fuse_cb.save() - git.stash.save('--', _stash_msg_fuse(self)) + 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, fuse_cb=None): - s_id = _stash_id(_stash_msg_fuse(self)) + 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 fuse_cb and fuse_cb.restore_ok: - fuse_cb.restore_ok() + if restore_fn: + restore_fn() except ErrorReturnCode: raise ApplyFailedError( 'Uncommitted changes failed to apply onto the new head of the ' @@ -979,9 +1064,6 @@ def update(): msg, get_tree_and_update_index(), # the commit tree parents) - if self.merge_in_progress: - self.gl_repo.git_repo.state_cleanup() - return self.gl_repo.git_repo[ci_oid] def publish(self, branch): @@ -1009,7 +1091,7 @@ def publish(self, branch): raise GlError(err_msg) - # Some helpers for checking preconditions + # Branch helpers def _check_op_not_in_progress(self): if self.merge_in_progress: @@ -1020,55 +1102,45 @@ def _check_op_not_in_progress(self): def _check_is_current(self): if not self.is_current: raise BranchIsCurrentError( - 'Branch "{0}" is the current branch'.format(self.branch_name)) - + 'Branch {0} is the current branch'.format(self.branch_name)) -# Some helpers for stashing -def _stash_id(msg): - """Gets the stash id of the stash with the given msg. - - Args: - msg: the message of the stash to retrieve. - - Returns: - the stash id of the stash with the given msg or None if no matching stash is - found. - """ - out = stdout(git.stash.list(grep=': {0}'.format(msg), _tty_out=False)) +# 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|*|', _tty_out=False)) if not out: - return None + return None, None - result = re.match(r'(stash@\{.+\}): ', out) + 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) - + return result.group(1).strip(), result.group(2).strip() def _stash_msg(name): - """Computes the stash msg to use for stashing changes with the given 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 -FuseCb = collections.namedtuple( - 'FuseCb', ['apply_ok', 'apply_err', 'save', 'restore_ok']) +# Misc +OpCb = collections.namedtuple( + 'OpCb', ['apply_ok', 'apply_err', 'save', 'restore_ok']) def stdout(p): return p.stdout.decode(ENCODING) - def stderr(p): return p.stderr.decode(ENCODING) - def walker(git_repo, target, reverse): flags = pygit2.GIT_SORT_TOPOLOGICAL | pygit2.GIT_SORT_TIME if reverse: diff --git a/gitless/tests/core/__init__.py b/gitless/tests/core/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/gitless/tests/core/common.py b/gitless/tests/core/common.py deleted file mode 100644 index c0c6eec..0000000 --- a/gitless/tests/core/common.py +++ /dev/null @@ -1,119 +0,0 @@ -# -*- coding: utf-8 -*- -# Gitless - a version control system built on top of Git. -# Licensed under GNU GPL v2. - -"""Common methods used in unit tests.""" - - -from __future__ import unicode_literals - -from functools import wraps -import os - -from sh import git - -from gitless import core -import gitless.tests.utils as utils_lib - - -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. - - The method decorated should be a unit test. - - Usage: - @common.assert_contents_unchanged('f1') - def test_method_that_shouldnt_modify_f1(self): - # do something here. - # assert something here. - - Args: - fps: the filepath(s) to assert. - """ - 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. - - The method decorated should be a unit test. - - Usage: - @common.assert_status_unchanged('f1') - def test_method_that_shouldnt_modify_f1_status(self): - # do something here. - # assert something here. - - Args: - fps: the filepath(s) to assert. - """ - 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. - - The method decorated should be a unit test. - - Usage: - @common.assert_no_side_effects('f1') - def test_method_that_shouldnt_affect_f1(self): - # do something here. - # assert something here. - - It is a shorthand of: - @common.assert_status_unchanged('f1') - @common.assert_contents_unchanged('f1') - def test_method_that_shouldnt_affect_f1(self): - # do something here. - # assert something here. - - Args: - fps: the filepath(s) to assert. - """ - def decorator(f): - @assert_contents_unchanged(*fps) - @assert_status_unchanged(*fps) - @wraps(f) - def wrapper(*args, **kwargs): - f(*args, **kwargs) - return wrapper - return decorator - - -# Private functions. - - -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 diff --git a/gitless/tests/core/test_branch.py b/gitless/tests/core/test_branch.py deleted file mode 100644 index 8c49815..0000000 --- a/gitless/tests/core/test_branch.py +++ /dev/null @@ -1,147 +0,0 @@ -# -*- coding: utf-8 -*- -# Gitless - a version control system built on top of Git. -# Licensed under GNU GPL v2. - -"""Unit tests for branch related operations.""" - - -from __future__ import unicode_literals - -import os - -from sh import git - -from gitless import core -import gitless.tests.utils as utils_lib - -from . import common - - -TRACKED_FP = 'f1' -TRACKED_FP_CONTENTS_1 = 'f1-1' -TRACKED_FP_CONTENTS_2 = 'f1-2' -UNTRACKED_FP = 'f2' -UNTRACKED_FP_CONTENTS = 'f2' -IGNORED_FP = 'f3' -BRANCH = 'b1' - - -class TestBranch(common.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 TestCreate(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 TestDelete(TestBranch): - - def test_delete(self): - self.repo.lookup_branch(BRANCH).delete() - self.assertRaises( - core.BranchIsCurrentError, - self.repo.lookup_branch('master').delete) - - -class TestSwitch(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)) diff --git a/gitless/tests/core/test_remote.py b/gitless/tests/core/test_remote.py deleted file mode 100644 index 4e1e23c..0000000 --- a/gitless/tests/core/test_remote.py +++ /dev/null @@ -1,127 +0,0 @@ -# -*- coding: utf-8 -*- -# Gitless - a version control system built on top of Git. -# Licensed under GNU GPL v2. - -"""Unit tests for remote related operations.""" - - -from __future__ import unicode_literals - -import io -from locale import getpreferredencoding -import os -import shutil -import tempfile - -from sh import git - -from gitless import core - -from . import common - - -ENCODING = getpreferredencoding() or 'utf-8' - -REMOTE_BRANCH = 'rb' - - -class TestRemote(common.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() - shutil.rmtree(self.remote_path) - - -class TestCreate(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 TestListAll(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 TestRm(TestRemote): - - def test_rm(self): - self.remotes.create('remote', self.remote_path) - self.remotes.delete('remote') - - def test_rm_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 TestSync(TestRemote): - - def setUp(self): - super(TestSync, self).setUp() - - with io.open('foo', mode='w', encoding=ENCODING) as f: - f.write('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) diff --git a/gitless/tests/core/test_file.py b/gitless/tests/test_core.py similarity index 68% rename from gitless/tests/core/test_file.py rename to gitless/tests/test_core.py index 632391b..366bd29 100644 --- a/gitless/tests/core/test_file.py +++ b/gitless/tests/test_core.py @@ -1,27 +1,29 @@ # -*- coding: utf-8 -*- -# Gitless - a version control system built on top of Git. -# Licensed under GNU GPL v2. +# Gitless - a version control system built on top of Git +# Licensed under GNU GPL v2 -"""Unit tests for file related operations.""" +"""Core unit tests.""" from __future__ import unicode_literals +from functools import wraps import os +import shutil +import tempfile from sh import git from gitless import core import gitless.tests.utils as utils_lib -from . import common - 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' @@ -48,9 +50,74 @@ 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.""" -class TestFile(common.TestCore): + 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): @@ -100,7 +167,7 @@ def setUp(self): self.curr_b = self.repo.current_branch -class TestTrackFile(TestFile): +class TestFileTrack(TestFile): def __assert_track_untracked(self, *fps): root = self.repo.root @@ -113,7 +180,7 @@ def __assert_track_untracked(self, *fps): 'Track of fp "{0}" failed: expected status.type={1}, got ' 'status.type={2}'.format(fp, core.GL_STATUS_TRACKED, st.type)) - @common.assert_contents_unchanged( + @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) @@ -123,7 +190,7 @@ def test_track_untracked(self): UNTRACKED_DIR_FP, UNTRACKED_DIR_FP_WITH_SPACE, UNTRACKED_DIR_DIR_FP, UNTRACKED_DIR_DIR_FP_WITH_SPACE) - @common.assert_contents_unchanged( + @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): @@ -143,7 +210,7 @@ def __assert_track_tracked(self, *fps): self.assertRaisesRegexp( ValueError, 'already tracked', self.curr_b.track_file, fp) - @common.assert_no_side_effects( + @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) @@ -153,7 +220,7 @@ def test_track_tracked_fp(self): TRACKED_DIR_FP, TRACKED_DIR_FP_WITH_SPACE, TRACKED_DIR_DIR_FP, TRACKED_DIR_DIR_FP_WITH_SPACE) - @common.assert_no_side_effects( + @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): @@ -183,12 +250,12 @@ def __assert_track_ignored(self, *fps): self.assertRaisesRegexp( ValueError, 'is ignored', self.curr_b.track_file, fp) - @common.assert_no_side_effects(IGNORED_FP, IGNORED_FP_WITH_SPACE) + @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 TestUntrackFile(TestFile): +class TestFileUntrack(TestFile): def __assert_untrack_tracked(self, *fps): root = self.repo.root @@ -201,7 +268,7 @@ def __assert_untrack_tracked(self, *fps): 'Untrack of fp "{0}" failed: expected status.type={1}, got ' 'status.type={2}'.format(fp, core.GL_STATUS_UNTRACKED, st.type)) - @common.assert_contents_unchanged( + @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) @@ -211,7 +278,7 @@ def test_untrack_tracked(self): TRACKED_DIR_FP, TRACKED_DIR_FP_WITH_SPACE, TRACKED_DIR_DIR_FP, TRACKED_DIR_DIR_FP_WITH_SPACE) - @common.assert_contents_unchanged( + @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): @@ -233,7 +300,7 @@ def __assert_untrack_error(self, msg, *fps): def __assert_untrack_untracked(self, *fps): self.__assert_untrack_error('already untracked', *fps) - @common.assert_no_side_effects( + @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) @@ -243,7 +310,7 @@ def test_untrack_untracked_fp(self): UNTRACKED_DIR_FP, UNTRACKED_DIR_FP_WITH_SPACE, UNTRACKED_DIR_DIR_FP, UNTRACKED_DIR_DIR_FP_WITH_SPACE) - @common.assert_contents_unchanged( + @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): @@ -256,7 +323,6 @@ def test_untrack_untracked_relative(self): 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: @@ -267,16 +333,15 @@ 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) - @common.assert_no_side_effects(IGNORED_FP, IGNORED_FP_WITH_SPACE) + @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 TestCheckoutFile(TestFile): +class TestFileCheckout(TestFile): def __assert_checkout_head(self, *fps): root = self.repo.root @@ -286,7 +351,7 @@ def __assert_checkout_head(self, *fps): os.path.relpath(fp, root), self.repo.revparse_single('HEAD')) self.assertEqual(TRACKED_FP_CONTENTS_2, utils_lib.read_file(fp)) - @common.assert_no_side_effects( + @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) @@ -296,7 +361,7 @@ def test_checkout_head(self): TRACKED_DIR_FP, TRACKED_DIR_FP_WITH_SPACE, TRACKED_DIR_DIR_FP, TRACKED_DIR_DIR_FP_WITH_SPACE) - @common.assert_no_side_effects( + @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) @@ -309,7 +374,6 @@ def test_checkout_head_relative(self): 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: @@ -335,7 +399,6 @@ def test_checkout_not_head_relative(self): 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') @@ -344,7 +407,7 @@ def __assert_checkout_error(self, *fps, **kwargs): KeyError, self.curr_b.checkout_file, os.path.relpath(fp, root), self.repo.revparse_single(cp)) - @common.assert_no_side_effects( + @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) @@ -362,22 +425,11 @@ def test_checkout_nonexistent(self): self.__assert_checkout_error(NONEXISTENT_FP, NONEXISTENT_FP_WITH_SPACE) -class TestStatus(TestFile): +class TestFileStatus(TestFile): def test_status_all(self): st_all = self.curr_b.status() - - # Some code is commented out because there's currently no way to get - # status to repot ignored and tracked unmodified files - - #seen = [] for fp, f_type, exists_at_head, exists_in_wd, modified, _ in st_all: - #if (fp == TRACKED_FP or fp == TRACKED_FP_WITH_SPACE or - # fp == TRACKED_DIR_FP or fp == TRACKED_DIR_FP_WITH_SPACE or - # fp == TRACKED_DIR_DIR_FP or fp == TRACKED_DIR_DIR_FP_WITH_SPACE): - # self.__assert_type(fp, core.GL_STATUS_TRACKED, f_type) - # self.__assert_field(fp, 'exists_at_head', True, exists_at_head) - # self.__assert_field(fp, 'modified', False, modified) 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 @@ -385,19 +437,11 @@ def test_status_all(self): 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 == IGNORED_FP or fp == IGNORED_FP_WITH_SPACE: - # self.__assert_type(fp, core.GL_STATUS_IGNORED, 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) - #else: - # self.fail('Unexpected fp {0}'.format(fp)) self.__assert_field(fp, 'exists_in_wd', True, exists_in_wd) - #seen.append(fp) - #self.assertItemsEqual(seen, ALL_FPS_IN_WD) def test_status_equivalence(self): for f_st in self.curr_b.status(): @@ -462,7 +506,7 @@ def test_status_unignore(self): 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. + # Tracked files can't be ignored st = self.curr_b.status_file(TRACKED_FP) self.assertEqual(core.GL_STATUS_TRACKED, st.type) @@ -501,7 +545,6 @@ def test_status_ignore_untracked(self): 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, @@ -515,11 +558,9 @@ def __assert_field(self, fp, field, expected, got): fp, field, expected, field, got)) -class TestDiffFile(TestFile): +class TestFileDiff(TestFile): - # TODO(sperezde): add DIR, DIR_DIR, relative tests to diff. - - @common.assert_status_unchanged( + @assert_status_unchanged( UNTRACKED_FP, UNTRACKED_FP_WITH_SPACE, IGNORED_FP, IGNORED_FP_WITH_SPACE) def test_diff_nontracked(self): @@ -545,7 +586,7 @@ def test_diff_nonexistent_fp(self): self.assertRaises( KeyError, self.curr_b.diff_file, NONEXISTENT_FP_WITH_SPACE) - @common.assert_no_side_effects(TRACKED_FP) + @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))) @@ -603,7 +644,7 @@ def test_diff_new_fp(self): 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. + # 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) @@ -654,15 +695,10 @@ def test_diff_non_ascii(self): self.assertEqual('new line', hunk.lines[1].content) - -FP_IN_CONFLICT = 'f_conflict' -DIR_FP_IN_CONFLICT = os.path.join(DIR, FP_IN_CONFLICT) - - -class TestResolveFile(TestFile): +class TestFileResolve(TestFile): def setUp(self): - super(TestResolveFile, self).setUp() + super(TestFileResolve, self).setUp() # Generate a conflict git.checkout(b='branch') @@ -677,23 +713,21 @@ def setUp(self): git.commit(FP_IN_CONFLICT, DIR_FP_IN_CONFLICT, m='master') git.merge('branch', _ok_code=[1]) - @common.assert_no_side_effects(TRACKED_FP) + @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) - @common.assert_contents_unchanged(FP_IN_CONFLICT, DIR_FP_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) @@ -702,3 +736,227 @@ def test_resolve_relative(self): 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() + shutil.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) diff --git a/gitless/tests/test_e2e.py b/gitless/tests/test_e2e.py index a7324c7..cfd9e55 100755 --- a/gitless/tests/test_e2e.py +++ b/gitless/tests/test_e2e.py @@ -353,7 +353,7 @@ def test_diff_non_ascii(self): self.assertEqual(out1, out2) -class TestFuse(TestEndToEnd): +class TestOp(TestEndToEnd): COMMITS_NUMBER = 4 OTHER = 'other' @@ -361,7 +361,7 @@ class TestFuse(TestEndToEnd): OTHER_FILE = 'other_file' def setUp(self): - super(TestFuse, self).setUp() + super(TestOp, self).setUp() self.commits = {} def create_commits(branch_name, fp): @@ -382,6 +382,9 @@ def create_commits(branch_name, fp): create_commits(self.OTHER, self.OTHER_FILE) gl.switch('master') + +class TestFuse(TestOp): + def __assert_history(self, expected): out = utils.stdout(gl.history(_tty_out=False)) cids = list(reversed(re.findall(r'ci (.*) in (.*)', out, re.UNICODE))) @@ -464,6 +467,38 @@ def trigger_conflicts(): 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') @@ -560,6 +595,16 @@ def test_uncommitted_tracked_changes_that_conflict_append(self): # 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')) + + class TestPerformance(TestEndToEnd): FPS_QTY = 10000 @@ -571,11 +616,9 @@ def setUp(self): utils.write_file(fp, fp) def test_status_performance(self): - """Assert that gl status is not too slow.""" - def assert_status_performance(): - # The test fails if gl status takes more than 100 times - # the time git status took. + # The test fails if `gl status` takes more than 100 times + # the time `git status` took. MAX_TOLERANCE = 100 t = time.time() @@ -599,7 +642,6 @@ def assert_status_performance(): assert_status_performance() def test_branch_switch_performance(self): - """Assert that switching branches is not too slow.""" MAX_TOLERANCE = 100 gl.commit(o='f1', m='commit') diff --git a/gl.spec b/gl.spec index e88a512..d8f5e7f 100644 --- a/gl.spec +++ b/gl.spec @@ -3,7 +3,7 @@ import os a = Analysis(['gl.py'], pathex=[os.getcwd()], - hiddenimports=['pygit2_cffi_3adedda7x2a59b7ee'], + hiddenimports=['pygit2_cffi_51591433xe8494016'], hookspath=None, runtime_hooks=None) diff --git a/setup.py b/setup.py index 21fdbfd..2526656 100755 --- a/setup.py +++ b/setup.py @@ -7,7 +7,7 @@ from setuptools import setup -VERSION = '0.8' +VERSION = '0.8.1' # Build helper @@ -36,7 +36,7 @@ sys.exit() -reqs = ['pygit2==0.22.1', 'sh==1.11', 'clint==0.3.6'] +reqs = ['pygit2==0.23.0', 'sh==1.11', 'clint==0.3.6'] if sys.version_info < (2, 7) or ( sys.version_info < (3, 3) and sys.version_info > (3, 0)): reqs.append('argparse')