From da9ca02b185d6cc95e41187a641fe3d0c77b6799 Mon Sep 17 00:00:00 2001 From: Jesse Luehrs Date: Sun, 8 Oct 2023 13:13:24 -0400 Subject: remove some no longer used config --- abook/.abook/abookrc | 39 - local/.bin/clean-vim-undo | 13 - local/.bin/git/git-imerge | 4135 ----------------------------------- local/.bin/hornet/twitch | 9 - local/.bin/nopaste | 37 - local/.bin/pick-music | 6 - local/.bin/rand-music | 13 - local/.bin/ringtone | 20 - local/.bin/tozt/learn_spam | 38 - local/.bin/update-addressbook | 276 --- mpd/.config/mpd/mpd.conf | 5 - mpd/.services/available/mpd/log/run | 4 - mpd/.services/available/mpd/run | 3 - mpd/Makefile | 6 - ncmpcpp/.config/ncmpcpp/bindings | 591 ----- packages.hornet | 4 - packages.mail | 2 - packages.mz-doy1 | 1 - packages.partofme | 2 - packages.root | 1 - packages.tozt | 2 - screen/.screenrc | 28 - 22 files changed, 5235 deletions(-) delete mode 100644 abook/.abook/abookrc delete mode 100755 local/.bin/clean-vim-undo delete mode 100644 local/.bin/git/git-imerge delete mode 100755 local/.bin/hornet/twitch delete mode 100755 local/.bin/nopaste delete mode 100755 local/.bin/pick-music delete mode 100755 local/.bin/rand-music delete mode 100755 local/.bin/ringtone delete mode 100755 local/.bin/tozt/learn_spam delete mode 100755 local/.bin/update-addressbook delete mode 100644 mpd/.config/mpd/mpd.conf delete mode 100755 mpd/.services/available/mpd/log/run delete mode 100755 mpd/.services/available/mpd/run delete mode 100644 mpd/Makefile delete mode 100644 ncmpcpp/.config/ncmpcpp/bindings delete mode 100644 screen/.screenrc diff --git a/abook/.abook/abookrc b/abook/.abook/abookrc deleted file mode 100644 index 671efa9..0000000 --- a/abook/.abook/abookrc +++ /dev/null @@ -1,39 +0,0 @@ -field aim = AIM -field irc = IRC, list -field twitter = Twitter -field skype = Skype -field birthday = Birthday, date - -field 2address = Address -field 2address2 = Address2 -field 2city = City -field 2state = State/Province -field 2zip = "ZIP/Postal Code" -field 2country = Country - -field textphone = Text - -view Contact = name, email, birthday, nick -view Phone = mobile, textphone, phone, workphone -view Internet = irc, aim, skype, twitter, url -view Address = address, address2, city, state, zip, country -view Address2 = 2address, 2address2, 2city, 2state, 2zip, 2country -view Other = notes - -set index_format=" {name:23} {email:34} {mobile:12|phone|workphone} {birthday:10} {nick}" - -set preserve_fields=all -set show_all_emails=false -set www_command=firefox -set address_style=us -set sort_field=name - -set use_colors=true -set color_header_bg=default -set color_header_fg=green -set color_footer_bg=default -set color_footer_fg=green -set color_list_header_bg=default -set color_list_highlight_bg=cyan -set color_list_even_fg=yellow -set color_list_odd_fg=yellow diff --git a/local/.bin/clean-vim-undo b/local/.bin/clean-vim-undo deleted file mode 100755 index efcd3aa..0000000 --- a/local/.bin/clean-vim-undo +++ /dev/null @@ -1,13 +0,0 @@ -#!/usr/bin/env perl -use strict; -use warnings; - -my $undodir = "$ENV{HOME}/.cache/vim/undo"; -opendir my $undo, $undodir - or die "can't open $undodir: $!"; - -for my $undofile (readdir($undo)) { - next unless -f "$undodir/$undofile"; - (my $file = $undofile) =~ s{%}{/}g; - unlink "$undodir/$undofile" unless -e $file && $file =~ m{^/}; -} diff --git a/local/.bin/git/git-imerge b/local/.bin/git/git-imerge deleted file mode 100644 index b903539..0000000 --- a/local/.bin/git/git-imerge +++ /dev/null @@ -1,4135 +0,0 @@ -#! /usr/bin/env python -# -*- coding: utf-8 -*- - -# Copyright 2012-2013 Michael Haggerty -# -# This file is part of git-imerge. -# -# git-imerge is free software: you can redistribute it and/or -# modify it under the terms of the GNU General Public License as -# published by the Free Software Foundation, either version 2 of the -# License, or (at your option) any later version. -# -# This program is distributed in the hope that it will be useful, but -# WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU -# General Public License for more details. -# -# You should have received a copy of the GNU General Public License -# along with this program. If not, see -# . - -r"""Git incremental merge - -Perform the merge between two branches incrementally. If conflicts -are encountered, figure out exactly which pairs of commits conflict, -and present the user with one pairwise conflict at a time for -resolution. - -Multiple incremental merges can be in progress at the same time. Each -incremental merge has a name, and its progress is recorded in the Git -repository as references under 'refs/imerge/NAME'. - -An incremental merge can be interrupted and resumed arbitrarily, or -even pushed to a server to allow somebody else to work on it. - - -Instructions: - -To start an incremental merge or rebase, use one of the following -commands: - - git-imerge merge BRANCH - Analogous to "git merge BRANCH" - - git-imerge rebase BRANCH - Analogous to "git rebase BRANCH" - - git-imerge drop [commit | commit1..commit2] - Drop the specified commit(s) from the current branch - - git-imerge revert [commit | commit1..commit2] - Revert the specified commits by adding new commits that - reverse their effects - - git-imerge start --name=NAME --goal=GOAL BRANCH - Start a general imerge - -Then the tool will present conflicts to you one at a time, similar to -"git rebase --incremental". Resolve each conflict, and then - - git add FILE... - git-imerge continue - -You can view your progress at any time with - - git-imerge diagram - -When you have resolved all of the conflicts, simplify and record the -result by typing - - git-imerge finish - -To get more help about any git-imerge subcommand, type - - git-imerge SUBCOMMAND --help - -""" - -from __future__ import absolute_import -from __future__ import division -from __future__ import print_function -from __future__ import unicode_literals - -import locale -import sys -import re -import subprocess -from subprocess import CalledProcessError -from subprocess import check_call -import itertools -import argparse -from io import StringIO -import json -import os - - -PREFERRED_ENCODING = locale.getpreferredencoding() - - -# Define check_output() for ourselves, including decoding of the -# output into PREFERRED_ENCODING: -def check_output(*popenargs, **kwargs): - if 'stdout' in kwargs: - raise ValueError('stdout argument not allowed, it will be overridden.') - process = subprocess.Popen(stdout=subprocess.PIPE, *popenargs, **kwargs) - output = communicate(process)[0] - retcode = process.poll() - if retcode: - cmd = kwargs.get("args") - if cmd is None: - cmd = popenargs[0] - # We don't store output in the CalledProcessError because - # the "output" keyword parameter was not supported in - # Python 2.6: - raise CalledProcessError(retcode, cmd) - return output - - -STATE_VERSION = (1, 3, 0) - -ZEROS = '0' * 40 - -ALLOWED_GOALS = [ - 'full', - 'rebase', - 'rebase-with-history', - 'border', - 'border-with-history', - 'border-with-history2', - 'merge', - 'drop', - 'revert', - ] -DEFAULT_GOAL = 'merge' - - -class Failure(Exception): - """An exception that indicates a normal failure of the script. - - Failures are reported at top level via sys.exit(str(e)) rather - than via a Python stack dump.""" - - pass - - -class AnsiColor: - BLACK = '\033[0;30m' - RED = '\033[0;31m' - GREEN = '\033[0;32m' - YELLOW = '\033[0;33m' - BLUE = '\033[0;34m' - MAGENTA = '\033[0;35m' - CYAN = '\033[0;36m' - B_GRAY = '\033[0;37m' - D_GRAY = '\033[1;30m' - B_RED = '\033[1;31m' - B_GREEN = '\033[1;32m' - B_YELLOW = '\033[1;33m' - B_BLUE = '\033[1;34m' - B_MAGENTA = '\033[1;35m' - B_CYAN = '\033[1;36m' - WHITE = '\033[1;37m' - END = '\033[0m' - - @classmethod - def disable(cls): - cls.BLACK = '' - cls.RED = '' - cls.GREEN = '' - cls.YELLOW = '' - cls.BLUE = '' - cls.MAGENTA = '' - cls.CYAN = '' - cls.B_GRAY = '' - cls.D_GRAY = '' - cls.B_RED = '' - cls.B_GREEN = '' - cls.B_YELLOW = '' - cls.B_BLUE = '' - cls.B_MAGENTA = '' - cls.B_CYAN = '' - cls.WHITE = '' - cls.END = '' - - -def iter_neighbors(iterable): - """For an iterable (x0, x1, x2, ...) generate [(x0,x1), (x1,x2), ...].""" - - i = iter(iterable) - - try: - last = next(i) - except StopIteration: - return - - for x in i: - yield (last, x) - last = x - - -def find_first_false(f, lo, hi): - """Return the smallest i in lo <= i < hi for which f(i) returns False using bisection. - - If there is no such i, return hi. - - """ - - # Loop invariant: f(i) returns True for i < lo; f(i) returns False - # for i >= hi. - - while lo < hi: - mid = (lo + hi) // 2 - if f(mid): - lo = mid + 1 - else: - hi = mid - - return lo - - -def call_silently(cmd): - try: - NULL = open(os.devnull, 'w') - except (IOError, AttributeError): - NULL = subprocess.PIPE - - p = subprocess.Popen(cmd, stdout=NULL, stderr=NULL) - p.communicate() - retcode = p.wait() - if retcode: - raise CalledProcessError(retcode, cmd) - - -def communicate(process, input=None): - """Return decoded output from process.""" - if input is not None: - input = input.encode(PREFERRED_ENCODING) - - output, error = process.communicate(input) - - output = None if output is None else output.decode(PREFERRED_ENCODING) - error = None if error is None else error.decode(PREFERRED_ENCODING) - - return (output, error) - - -if sys.hexversion < 0x03000000: - # In Python 2.x, os.environ keys and values must be byte - # strings: - def env_encode(s): - """Encode unicode keys or values for use in os.environ.""" - - return s.encode(PREFERRED_ENCODING) - -else: - # In Python 3.x, os.environ keys and values must be unicode - # strings: - def env_encode(s): - """Use unicode keys or values unchanged in os.environ.""" - - return s - - -class UncleanWorkTreeError(Failure): - pass - - -class AutomaticMergeFailed(Exception): - def __init__(self, commit1, commit2): - Exception.__init__( - self, 'Automatic merge of %s and %s failed' % (commit1, commit2,) - ) - self.commit1, self.commit2 = commit1, commit2 - - -class InvalidBranchNameError(Failure): - pass - - -class NotFirstParentAncestorError(Failure): - def __init__(self, commit1, commit2): - Failure.__init__( - self, - 'Commit "%s" is not a first-parent ancestor of "%s"' - % (commit1, commit2), - ) - - -class NonlinearAncestryError(Failure): - def __init__(self, commit1, commit2): - Failure.__init__( - self, - 'The history "%s..%s" is not linear' - % (commit1, commit2), - ) - - -class NothingToDoError(Failure): - def __init__(self, src_tip, dst_tip): - Failure.__init__( - self, - 'There are no commits on "%s" that are not already in "%s"' - % (src_tip, dst_tip), - ) - - -class GitTemporaryHead(object): - """A context manager that records the current HEAD state then restores it. - - This should only be used when the working copy is clean. message - is used for the reflog. - - """ - - def __init__(self, git, message): - self.git = git - self.message = message - - def __enter__(self): - self.head_name = self.git.get_head_refname() - return self - - def __exit__(self, exc_type, exc_val, exc_tb): - if self.head_name: - try: - self.git.restore_head(self.head_name, self.message) - except CalledProcessError as e: - raise Failure( - 'Could not restore HEAD to %r!: %s\n' - % (self.head_name, e.message,) - ) - - return False - - -class GitRepository(object): - BRANCH_PREFIX = 'refs/heads/' - - MERGE_STATE_REFNAME_RE = re.compile( - r""" - ^ - refs\/imerge\/ - (?P.+) - \/state - $ - """, - re.VERBOSE, - ) - - def __init__(self): - self.git_dir_cache = None - - def git_dir(self): - if self.git_dir_cache is None: - self.git_dir_cache = check_output( - ['git', 'rev-parse', '--git-dir'] - ).rstrip('\n') - - return self.git_dir_cache - - def check_imerge_name_format(self, name): - """Check that name is a valid imerge name.""" - - try: - call_silently( - ['git', 'check-ref-format', 'refs/imerge/%s' % (name,)] - ) - except CalledProcessError: - raise Failure('Name %r is not a valid refname component!' % (name,)) - - def check_branch_name_format(self, name): - """Check that name is a valid branch name.""" - - try: - call_silently( - ['git', 'check-ref-format', 'refs/heads/%s' % (name,)] - ) - except CalledProcessError: - raise InvalidBranchNameError('Name %r is not a valid branch name!' % (name,)) - - def iter_existing_imerge_names(self): - """Iterate over the names of existing MergeStates in this repo.""" - - for line in check_output(['git', 'for-each-ref', 'refs/imerge']).splitlines(): - (sha1, type, refname) = line.split() - if type == 'blob': - m = GitRepository.MERGE_STATE_REFNAME_RE.match(refname) - if m: - yield m.group('name') - - def set_default_imerge_name(self, name): - """Set the default merge to the specified one. - - name can be None to cause the default to be cleared.""" - - if name is None: - try: - check_call(['git', 'config', '--unset', 'imerge.default']) - except CalledProcessError as e: - if e.returncode == 5: - # Value was not set - pass - else: - raise - else: - check_call(['git', 'config', 'imerge.default', name]) - - def get_default_imerge_name(self): - """Get the name of the default merge, or None if it is currently unset.""" - - try: - return check_output(['git', 'config', 'imerge.default']).rstrip() - except CalledProcessError: - return None - - def get_default_edit(self): - """Should '--edit' be used when committing intermediate user merges? - - When 'git imerge continue' or 'git imerge record' finds a user - merge that can be committed, should it (by default) ask the user - to edit the commit message? This behavior can be configured via - 'imerge.editmergemessages'. If it is not configured, return False. - - Please note that this function is only used to choose the default - value. It can be overridden on the command line using '--edit' or - '--no-edit'. - - """ - - try: - return {'true' : True, 'false' : False}[ - check_output( - ['git', 'config', '--bool', 'imerge.editmergemessages'] - ).rstrip() - ] - except CalledProcessError: - return False - - def unstaged_changes(self): - """Return True iff there are unstaged changes in the working copy""" - - try: - check_call(['git', 'diff-files', '--quiet', '--ignore-submodules']) - return False - except CalledProcessError: - return True - - def uncommitted_changes(self): - """Return True iff the index contains uncommitted changes.""" - - try: - check_call([ - 'git', 'diff-index', '--cached', '--quiet', - '--ignore-submodules', 'HEAD', '--', - ]) - return False - except CalledProcessError: - return True - - def get_commit_sha1(self, arg): - """Convert arg into a SHA1 and verify that it refers to a commit. - - If not, raise ValueError.""" - - try: - return self.rev_parse('%s^{commit}' % (arg,)) - except CalledProcessError: - raise ValueError('%r does not refer to a valid git commit' % (arg,)) - - def refresh_index(self): - process = subprocess.Popen( - ['git', 'update-index', '-q', '--ignore-submodules', '--refresh'], - stdout=subprocess.PIPE, stderr=subprocess.PIPE, - ) - out, err = communicate(process) - retcode = process.poll() - if retcode: - raise UncleanWorkTreeError(err.rstrip() or out.rstrip()) - - def verify_imerge_name_available(self, name): - self.check_imerge_name_format(name) - if check_output(['git', 'for-each-ref', 'refs/imerge/%s' % (name,)]): - raise Failure('Name %r is already in use!' % (name,)) - - def check_imerge_exists(self, name): - """Verify that a MergeState with the given name exists. - - Just check for the existence, readability, and compatible - version of the 'state' reference. If the reference doesn't - exist, just return False. If it exists but is unusable for - some other reason, raise an exception.""" - - self.check_imerge_name_format(name) - state_refname = 'refs/imerge/%s/state' % (name,) - for line in check_output(['git', 'for-each-ref', state_refname]).splitlines(): - (sha1, type, refname) = line.split() - if refname == state_refname and type == 'blob': - self.read_imerge_state_dict(name) - # If that didn't throw an exception: - return True - else: - return False - - def read_imerge_state_dict(self, name): - state_string = check_output( - ['git', 'cat-file', 'blob', 'refs/imerge/%s/state' % (name,)], - ) - state = json.loads(state_string) - - # Convert state['version'] to a tuple of integers, and verify - # that it is compatible with this version of the script: - version = tuple(int(i) for i in state['version'].split('.')) - if version[0] != STATE_VERSION[0] or version[1] > STATE_VERSION[1]: - raise Failure( - 'The format of imerge %s (%s) is not compatible with this script version.' - % (name, state['version'],) - ) - state['version'] = version - - return state - - def read_imerge_state(self, name): - """Read the state associated with the specified imerge. - - Return the tuple - - (state_dict, {(i1, i2) : (sha1, source), ...}) - - , where source is 'auto' or 'manual'. Validity is checked only - lightly. - - """ - - merge_ref_re = re.compile( - r""" - ^ - refs\/imerge\/ - """ + re.escape(name) + r""" - \/(?Pauto|manual)\/ - (?P0|[1-9][0-9]*) - \- - (?P0|[1-9][0-9]*) - $ - """, - re.VERBOSE, - ) - - state_ref_re = re.compile( - r""" - ^ - refs\/imerge\/ - """ + re.escape(name) + r""" - \/state - $ - """, - re.VERBOSE, - ) - - state = None - - # A map {(i1, i2) : (sha1, source)}: - merges = {} - - # refnames that were found but not understood: - unexpected = [] - - for line in check_output([ - 'git', 'for-each-ref', 'refs/imerge/%s' % (name,) - ]).splitlines(): - (sha1, type, refname) = line.split() - m = merge_ref_re.match(refname) - if m: - if type != 'commit': - raise Failure('Reference %r is not a commit!' % (refname,)) - i1, i2 = int(m.group('i1')), int(m.group('i2')) - source = m.group('source') - merges[i1, i2] = (sha1, source) - continue - - m = state_ref_re.match(refname) - if m: - if type != 'blob': - raise Failure('Reference %r is not a blob!' % (refname,)) - state = self.read_imerge_state_dict(name) - continue - - unexpected.append(refname) - - if state is None: - raise Failure( - 'No state found; it should have been a blob reference at ' - '"refs/imerge/%s/state"' % (name,) - ) - - if unexpected: - raise Failure( - 'Unexpected reference(s) found in "refs/imerge/%s" namespace:\n %s\n' - % (name, '\n '.join(unexpected),) - ) - - return (state, merges) - - def write_imerge_state_dict(self, name, state): - state_string = json.dumps(state, sort_keys=True) + '\n' - - cmd = ['git', 'hash-object', '-t', 'blob', '-w', '--stdin'] - p = subprocess.Popen(cmd, stdin=subprocess.PIPE, stdout=subprocess.PIPE) - out = communicate(p, input=state_string)[0] - retcode = p.poll() - if retcode: - raise CalledProcessError(retcode, cmd) - sha1 = out.strip() - check_call([ - 'git', 'update-ref', - '-m', 'imerge %r: Record state' % (name,), - 'refs/imerge/%s/state' % (name,), - sha1, - ]) - - def is_ancestor(self, commit1, commit2): - """Return True iff commit1 is an ancestor (or equal to) commit2.""" - - if commit1 == commit2: - return True - else: - return int( - check_output([ - 'git', 'rev-list', '--count', '--ancestry-path', - '%s..%s' % (commit1, commit2,), - ]).strip() - ) != 0 - - def is_ff(self, refname, commit): - """Would updating refname to commit be a fast-forward update? - - Return True iff refname is not currently set or it points to an - ancestor of commit. - - """ - - try: - ref_oldval = self.get_commit_sha1(refname) - except ValueError: - # refname doesn't already exist; no problem. - return True - else: - return self.is_ancestor(ref_oldval, commit) - - def automerge(self, commit1, commit2, msg=None): - """Attempt an automatic merge of commit1 and commit2. - - Return the SHA1 of the resulting commit, or raise - AutomaticMergeFailed on error. This must be called with a clean - worktree.""" - - call_silently(['git', 'checkout', '-f', commit1]) - cmd = ['git', '-c', 'rerere.enabled=false', 'merge'] - if msg is not None: - cmd += ['-m', msg] - cmd += [commit2] - try: - call_silently(cmd) - except CalledProcessError: - self.abort_merge() - raise AutomaticMergeFailed(commit1, commit2) - else: - return self.get_commit_sha1('HEAD') - - def manualmerge(self, commit, msg): - """Initiate a merge of commit into the current HEAD.""" - - check_call(['git', 'merge', '--no-commit', '-m', msg, commit,]) - - def require_clean_work_tree(self, action): - """Verify that the current tree is clean. - - The code is a Python translation of the git-sh-setup(1) function - of the same name.""" - - process = subprocess.Popen( - ['git', 'rev-parse', '--verify', 'HEAD'], - stdout=subprocess.PIPE, stderr=subprocess.PIPE, - ) - err = communicate(process)[1] - retcode = process.poll() - if retcode: - raise UncleanWorkTreeError(err.rstrip()) - - self.refresh_index() - - error = [] - if self.unstaged_changes(): - error.append('Cannot %s: You have unstaged changes.' % (action,)) - - if self.uncommitted_changes(): - if not error: - error.append('Cannot %s: Your index contains uncommitted changes.' % (action,)) - else: - error.append('Additionally, your index contains uncommitted changes.') - - if error: - raise UncleanWorkTreeError('\n'.join(error)) - - def simple_merge_in_progress(self): - """Return True iff a merge (of a single branch) is in progress.""" - - try: - with open(os.path.join(self.git_dir(), 'MERGE_HEAD')) as f: - heads = [line.rstrip() for line in f] - except IOError: - return False - - return len(heads) == 1 - - def commit_user_merge(self, edit_log_msg=None): - """If a merge is in progress and ready to be committed, commit it. - - If a simple merge is in progress and any changes in the working - tree are staged, commit the merge commit and return True. - Otherwise, return False. - - """ - - if not self.simple_merge_in_progress(): - return False - - # Check if all conflicts are resolved and everything in the - # working tree is staged: - self.refresh_index() - if self.unstaged_changes(): - raise UncleanWorkTreeError( - 'Cannot proceed: You have unstaged changes.' - ) - - # A merge is in progress, and either all changes have been staged - # or no changes are necessary. Create a merge commit. - cmd = ['git', 'commit', '--no-verify'] - - if edit_log_msg is None: - edit_log_msg = self.get_default_edit() - - if edit_log_msg: - cmd += ['--edit'] - else: - cmd += ['--no-edit'] - - try: - check_call(cmd) - except CalledProcessError: - raise Failure('Could not commit staged changes.') - - return True - - def create_commit_chain(self, base, path): - """Point refname at the chain of commits indicated by path. - - path is a list [(commit, metadata), ...]. Create a series of - commits corresponding to the entries in path. Each commit's tree - is taken from the corresponding old commit, and each commit's - metadata is taken from the corresponding metadata commit. Use base - as the parent of the first commit, or make the first commit a root - commit if base is None. Reuse existing commits from the list - whenever possible. - - Return a commit object corresponding to the last commit in the - chain. - - """ - - reusing = True - if base is None: - if not path: - raise ValueError('neither base nor path specified') - parents = [] - else: - parents = [base] - - for (commit, metadata) in path: - if reusing: - if commit == metadata and self.get_commit_parents(commit) == parents: - # We can reuse this commit, too. - parents = [commit] - continue - else: - reusing = False - - # Create a commit, copying the old log message and author info - # from the metadata commit: - tree = self.get_tree(commit) - new_commit = self.commit_tree( - tree, parents, - msg=self.get_log_message(metadata), - metadata=self.get_author_info(metadata), - ) - parents = [new_commit] - - [commit] = parents - return commit - - def rev_parse(self, arg): - return check_output(['git', 'rev-parse', '--verify', '--quiet', arg]).strip() - - def rev_list(self, *args): - cmd = ['git', 'rev-list'] + list(args) - return [ - l.strip() - for l in check_output(cmd).splitlines() - ] - - def rev_list_with_parents(self, *args): - """Iterate over (commit, [parent,...]).""" - - cmd = ['git', 'log', '--format=%H %P'] + list(args) - for line in check_output(cmd).splitlines(): - commits = line.strip().split() - yield (commits[0], commits[1:]) - - def summarize_commit(self, commit): - """Summarize `commit` to stdout.""" - - check_call(['git', '--no-pager', 'log', '--no-walk', commit]) - - def get_author_info(self, commit): - """Return environment settings to set author metadata. - - Return a map {str : str}.""" - - # We use newlines as separators here because msysgit has problems - # with NUL characters; see - # - # https://github.com/mhagger/git-imerge/pull/71 - a = check_output([ - 'git', '--no-pager', 'log', '-n1', - '--format=%an%n%ae%n%ai', commit - ]).strip().splitlines() - - return { - 'GIT_AUTHOR_NAME': env_encode(a[0]), - 'GIT_AUTHOR_EMAIL': env_encode(a[1]), - 'GIT_AUTHOR_DATE': env_encode(a[2]), - } - - def get_log_message(self, commit): - contents = check_output([ - 'git', 'cat-file', 'commit', commit, - ]).splitlines(True) - contents = contents[contents.index('\n') + 1:] - if contents and contents[-1][-1:] != '\n': - contents.append('\n') - return ''.join(contents) - - def get_commit_parents(self, commit): - """Return a list containing the parents of commit.""" - - return check_output( - ['git', '--no-pager', 'log', '--no-walk', '--pretty=format:%P', commit] - ).strip().split() - - def get_tree(self, arg): - return self.rev_parse('%s^{tree}' % (arg,)) - - def update_ref(self, refname, value, msg, deref=True): - if deref: - opt = [] - else: - opt = ['--no-deref'] - - check_call(['git', 'update-ref'] + opt + ['-m', msg, refname, value]) - - def delete_ref(self, refname, msg, deref=True): - if deref: - opt = [] - else: - opt = ['--no-deref'] - - check_call(['git', 'update-ref'] + opt + ['-m', msg, '-d', refname]) - - def delete_imerge_refs(self, name): - stdin = ''.join( - 'delete %s\n' % (refname,) - for refname in check_output([ - 'git', 'for-each-ref', - '--format=%(refname)', - 'refs/imerge/%s' % (name,) - ]).splitlines() - ) - - process = subprocess.Popen( - [ - 'git', 'update-ref', - '-m', 'imerge: remove merge %r' % (name,), - '--stdin', - ], - stdin=subprocess.PIPE, - stdout=subprocess.PIPE, stderr=subprocess.STDOUT, - ) - out = communicate(process, input=stdin)[0] - retcode = process.poll() - if retcode: - sys.stderr.write( - 'Warning: error removing references:\n%s' % (out,) - ) - - def detach(self, msg): - """Detach HEAD. msg is used for the reflog.""" - - self.update_ref('HEAD', 'HEAD^0', msg, deref=False) - - def reset_hard(self, commit): - check_call(['git', 'reset', '--hard', commit]) - - def amend(self): - check_call(['git', 'commit', '--amend']) - - def abort_merge(self): - # We don't use "git merge --abort" here because it was - # only added in git version 1.7.4. - check_call(['git', 'reset', '--merge']) - - def compute_best_merge_base(self, tip1, tip2): - try: - merge_bases = check_output(['git', 'merge-base', '--all', tip1, tip2]).splitlines() - except CalledProcessError: - raise Failure('Cannot compute merge base for %r and %r' % (tip1, tip2)) - if not merge_bases: - raise Failure('%r and %r do not have a common merge base' % (tip1, tip2)) - if len(merge_bases) == 1: - return merge_bases[0] - - # There are multiple merge bases. The "best" one is the one that - # is the "closest" to the tips, which we define to be the one with - # the fewest non-merge commits in "merge_base..tip". (It can be - # shown that the result is independent of which tip is used in the - # computation.) - best_base = best_count = None - for merge_base in merge_bases: - cmd = ['git', 'rev-list', '--no-merges', '--count', '%s..%s' % (merge_base, tip1)] - count = int(check_output(cmd).strip()) - if best_base is None or count < best_count: - best_base = merge_base - best_count = count - - return best_base - - def linear_ancestry(self, commit1, commit2, first_parent): - """Compute a linear ancestry between commit1 and commit2. - - Our goal is to find a linear series of commits connecting - `commit1` and `commit2`. We do so as follows: - - * If all of the commits in - - git rev-list --ancestry-path commit1..commit2 - - are on a linear chain, return that. - - * If there are multiple paths between `commit1` and `commit2` in - that list of commits, then - - * If `first_parent` is not set, then raise an - `NonlinearAncestryError` exception. - - * If `first_parent` is set, then, at each merge commit, follow - the first parent that is in that list of commits. - - Return a list of SHA-1s in 'chronological' order. - - Raise NotFirstParentAncestorError if commit1 is not an ancestor of - commit2. - - """ - - oid1 = self.rev_parse(commit1) - oid2 = self.rev_parse(commit2) - - parentage = {oid1 : []} - for (commit, parents) in self.rev_list_with_parents( - '--ancestry-path', '--topo-order', '%s..%s' % (oid1, oid2) - ): - parentage[commit] = parents - - commits = [] - - commit = oid2 - while commit != oid1: - parents = parentage.get(commit, []) - - # Only consider parents that are in the ancestry path: - included_parents = [ - parent - for parent in parents - if parent in parentage - ] - - if not included_parents: - raise NotFirstParentAncestorError(commit1, commit2) - elif len(included_parents) == 1 or first_parent: - parent = included_parents[0] - else: - raise NonlinearAncestryError(commit1, commit2) - - commits.append(commit) - commit = parent - - commits.reverse() - - return commits - - def get_boundaries(self, tip1, tip2, first_parent): - """Get the boundaries of an incremental merge. - - Given the tips of two branches that should be merged, return - (merge_base, commits1, commits2) describing the edges of the - imerge. Raise Failure if there are any problems.""" - - merge_base = self.compute_best_merge_base(tip1, tip2) - - commits1 = self.linear_ancestry(merge_base, tip1, first_parent) - if not commits1: - raise NothingToDoError(tip1, tip2) - - commits2 = self.linear_ancestry(merge_base, tip2, first_parent) - if not commits2: - raise NothingToDoError(tip2, tip1) - - return (merge_base, commits1, commits2) - - def get_head_refname(self, short=False): - """Return the name of the reference that is currently checked out. - - If `short` is set, return it as a branch name. If HEAD is - currently detached, return None.""" - - cmd = ['git', 'symbolic-ref', '--quiet'] - if short: - cmd += ['--short'] - cmd += ['HEAD'] - try: - return check_output(cmd).strip() - except CalledProcessError: - return None - - def restore_head(self, refname, message): - check_call(['git', 'symbolic-ref', '-m', message, 'HEAD', refname]) - check_call(['git', 'reset', '--hard']) - - def checkout(self, refname, quiet=False): - cmd = ['git', 'checkout'] - if quiet: - cmd += ['--quiet'] - if refname.startswith(GitRepository.BRANCH_PREFIX): - target = refname[len(GitRepository.BRANCH_PREFIX):] - else: - target = '%s^0' % (refname,) - cmd += [target] - check_call(cmd) - - def commit_tree(self, tree, parents, msg, metadata=None): - """Create a commit containing the specified tree. - - metadata can be author or committer information to be added to the - environment, as str objects; e.g., {'GIT_AUTHOR_NAME' : 'me'}. - - Return the SHA-1 of the new commit object.""" - - cmd = ['git', 'commit-tree', tree] - for parent in parents: - cmd += ['-p', parent] - - if metadata is not None: - env = os.environ.copy() - env.update(metadata) - else: - env = os.environ - - process = subprocess.Popen( - cmd, env=env, stdin=subprocess.PIPE, stdout=subprocess.PIPE, - ) - out = communicate(process, input=msg)[0] - retcode = process.poll() - - if retcode: - # We don't store the output in the CalledProcessError because - # the "output" keyword parameter was not supported in Python - # 2.6: - raise CalledProcessError(retcode, cmd) - - return out.strip() - - def revert(self, commit): - """Apply the inverse of commit^..commit to HEAD and commit.""" - - cmd = ['git', 'revert', '--no-edit'] - if len(self.get_commit_parents(commit)) > 1: - cmd += ['-m', '1'] - cmd += [commit] - check_call(cmd) - - def reparent(self, commit, parent_sha1s, msg=None): - """Create a new commit object like commit, but with the specified parents. - - commit is the SHA1 of an existing commit and parent_sha1s is a - list of SHA1s. Create a new commit exactly like that one, except - that it has the specified parent commits. Return the SHA1 of the - resulting commit object, which is already stored in the object - database but is not yet referenced by anything. - - If msg is set, then use it as the commit message for the new - commit.""" - - old_commit = check_output(['git', 'cat-file', 'commit', commit]) - separator = old_commit.index('\n\n') - headers = old_commit[:separator + 1].splitlines(True) - rest = old_commit[separator + 2:] - - new_commit = StringIO() - for i in range(len(headers)): - line = headers[i] - if line.startswith('tree '): - new_commit.write(line) - for parent_sha1 in parent_sha1s: - new_commit.write('parent %s\n' % (parent_sha1,)) - elif line.startswith('parent '): - # Discard old parents: - pass - else: - new_commit.write(line) - - new_commit.write('\n') - if msg is None: - new_commit.write(rest) - else: - new_commit.write(msg) - if not msg.endswith('\n'): - new_commit.write('\n') - - process = subprocess.Popen( - ['git', 'hash-object', '-t', 'commit', '-w', '--stdin'], - stdin=subprocess.PIPE, stdout=subprocess.PIPE, - ) - out = communicate(process, input=new_commit.getvalue())[0] - retcode = process.poll() - if retcode: - raise Failure('Could not reparent commit %s' % (commit,)) - return out.strip() - - def temporary_head(self, message): - """Return a context manager to manage a temporary HEAD. - - On entry, record the current HEAD state. On exit, restore it. - message is used for the reflog. - - """ - - return GitTemporaryHead(self, message) - - -class MergeRecord(object): - # Bits for the flags field: - - # There is a saved successful auto merge: - SAVED_AUTO = 0x01 - - # An auto merge (which may have been unsuccessful) has been done: - NEW_AUTO = 0x02 - - # There is a saved successful manual merge: - SAVED_MANUAL = 0x04 - - # A manual merge (which may have been unsuccessful) has been done: - NEW_MANUAL = 0x08 - - # A merge that is currently blocking the merge frontier: - BLOCKED = 0x10 - - # Some useful bit combinations: - SAVED = SAVED_AUTO | SAVED_MANUAL - NEW = NEW_AUTO | NEW_MANUAL - - AUTO = SAVED_AUTO | NEW_AUTO - MANUAL = SAVED_MANUAL | NEW_MANUAL - - ALLOWED_INITIAL_FLAGS = [ - SAVED_AUTO, - SAVED_MANUAL, - NEW_AUTO, - NEW_MANUAL, - ] - - def __init__(self, sha1=None, flags=0): - # The currently believed correct merge, or None if it is - # unknown or the best attempt was unsuccessful. - self.sha1 = sha1 - - if self.sha1 is None: - if flags != 0: - raise ValueError('Initial flags (%s) for sha1=None should be 0' % (flags,)) - elif flags not in self.ALLOWED_INITIAL_FLAGS: - raise ValueError('Initial flags (%s) is invalid' % (flags,)) - - # See bits above. - self.flags = flags - - def record_merge(self, sha1, source): - """Record a merge at this position. - - source must be SAVED_AUTO, SAVED_MANUAL, NEW_AUTO, or NEW_MANUAL.""" - - if source == self.SAVED_AUTO: - # SAVED_AUTO is recorded in any case, but only used if it - # is the only info available. - if self.flags & (self.MANUAL | self.NEW) == 0: - self.sha1 = sha1 - self.flags |= source - elif source == self.NEW_AUTO: - # NEW_AUTO is silently ignored if any MANUAL value is - # already recorded. - if self.flags & self.MANUAL == 0: - self.sha1 = sha1 - self.flags |= source - elif source == self.SAVED_MANUAL: - # SAVED_MANUAL is recorded in any case, but only used if - # no NEW_MANUAL is available. - if self.flags & self.NEW_MANUAL == 0: - self.sha1 = sha1 - self.flags |= source - elif source == self.NEW_MANUAL: - # NEW_MANUAL is always used, and also causes NEW_AUTO to - # be forgotten if present. - self.sha1 = sha1 - self.flags = (self.flags | source) & ~self.NEW_AUTO - else: - raise ValueError('Undefined source: %s' % (source,)) - - def record_blocked(self, blocked): - if blocked: - self.flags |= self.BLOCKED - else: - self.flags &= ~self.BLOCKED - - def is_known(self): - return self.sha1 is not None - - def is_blocked(self): - return self.flags & self.BLOCKED != 0 - - def is_manual(self): - return self.flags & self.MANUAL != 0 - - def save(self, git, name, i1, i2): - """If this record has changed, save it.""" - - def set_ref(source): - git.update_ref( - 'refs/imerge/%s/%s/%d-%d' % (name, source, i1, i2), - self.sha1, - 'imerge %r: Record %s merge' % (name, source,), - ) - - def clear_ref(source): - git.delete_ref( - 'refs/imerge/%s/%s/%d-%d' % (name, source, i1, i2), - 'imerge %r: Remove obsolete %s merge' % (name, source,), - ) - - if self.flags & self.MANUAL: - if self.flags & self.AUTO: - # Any MANUAL obsoletes any AUTO: - if self.flags & self.SAVED_AUTO: - clear_ref('auto') - - self.flags &= ~self.AUTO - - if self.flags & self.NEW_MANUAL: - # Convert NEW_MANUAL to SAVED_MANUAL. - if self.sha1: - set_ref('manual') - self.flags |= self.SAVED_MANUAL - elif self.flags & self.SAVED_MANUAL: - # Delete any existing SAVED_MANUAL: - clear_ref('manual') - self.flags &= ~self.SAVED_MANUAL - self.flags &= ~self.NEW_MANUAL - - elif self.flags & self.NEW_AUTO: - # Convert NEW_AUTO to SAVED_AUTO. - if self.sha1: - set_ref('auto') - self.flags |= self.SAVED_AUTO - elif self.flags & self.SAVED_AUTO: - # Delete any existing SAVED_AUTO: - clear_ref('auto') - self.flags &= ~self.SAVED_AUTO - self.flags &= ~self.NEW_AUTO - - -class UnexpectedMergeFailure(Exception): - def __init__(self, msg, i1, i2): - Exception.__init__(self, msg) - self.i1, self.i2 = i1, i2 - - -class BlockCompleteError(Exception): - pass - - -class FrontierBlockedError(Exception): - def __init__(self, msg, i1, i2): - Exception.__init__(self, msg) - self.i1 = i1 - self.i2 = i2 - - -class NotABlockingCommitError(Exception): - pass - - -def find_frontier_blocks(block): - """Iterate over the frontier blocks for the specified block. - - Use bisection to find the blocks. Iterate over the blocks starting - in the bottom left and ending at the top right. Record in block - any blockers that we find. - - We make the following assumptions (using Python subscript - notation): - - 0. All of the merges in block[1:,0] and block[0,1:] are - already known. (This is an invariant of the Block class.) - - 1. If a direct merge can be done between block[i1-1,0] and - block[0,i2-1], then all of the pairwise merges in - block[1:i1, 1:i2] can also be done. - - 2. If a direct merge fails between block[i1-1,0] and - block[0,i2-1], then all of the pairwise merges in - block[i1-1:,i2-1:] would also fail. - - Under these assumptions, the merge frontier is a stepstair - pattern going from the bottom-left to the top-right, and - bisection can be used to find the transition between mergeable - and conflicting in any row or column. - - Of course these assumptions are not rigorously true, so the - MergeFrontier returned by this function is only an - approximation of the real merge diagram. We check for and - correct such inconsistencies later. - - """ - - # Given that these diagrams typically have few blocks, check - # the end of a range first to see if the whole range can be - # determined, and fall back to bisection otherwise. We - # determine the frontier block by block, starting in the lower - # left. - - if block.len1 <= 1 or block.len2 <= 1 or block.is_blocked(1, 1): - return - - if block.is_mergeable(block.len1 - 1, block.len2 - 1): - # The whole block is mergable! - yield block - return - - if not block.is_mergeable(1, 1): - # There are no mergeable blocks in block; therefore, - # block[1,1] must itself be unmergeable. Record that - # fact: - block[1, 1].record_blocked(True) - return - - # At this point, we know that there is at least one mergeable - # commit in the first column. Find the height of the success - # block in column 1: - i1 = 1 - i2 = find_first_false( - lambda i: block.is_mergeable(i1, i), - 2, block.len2, - ) - - # Now we know that (1,i2-1) is mergeable but (1,i2) is not; - # e.g., (i1, i2) is like 'A' (or maybe 'B') in the following - # diagram (where '*' means mergeable, 'x' means not mergeable, - # and '?' means indeterminate) and that the merge under 'A' is - # not mergeable: - # - # i1 - # - # 0123456 - # 0 ******* - # 1 **????? - # i2 2 **????? - # 3 **????? - # 4 *Axxxxx - # 5 *xxxxxx - # B - - while True: - if i2 == 1: - break - - # At this point in the loop, we know that any blocks to - # the left of 'A' have already been recorded, (i1, i2-1) - # is mergeable but (i1, i2) is not; e.g., we are at a - # place like 'A' in the following diagram: - # - # i1 - # - # 0123456 - # 0 **|**** - # 1 **|*??? - # i2 2 **|*??? - # 3 **|Axxx - # 4 --+xxxx - # 5 *xxxxxx - # - # This implies that (i1, i2) is the first unmergeable - # commit in a blocker block (though blocker blocks are not - # recorded explicitly). It also implies that a mergeable - # block has its last mergeable commit somewhere in row - # i2-1; find its width. - if ( - i1 == block.len1 - 1 - or block.is_mergeable(block.len1 - 1, i2 - 1) - ): - yield block[:block.len1, :i2] - break - else: - i1 = find_first_false( - lambda i: block.is_mergeable(i, i2 - 1), - i1 + 1, block.len1 - 1, - ) - yield block[:i1, :i2] - - # At this point in the loop, (i1-1, i2-1) is mergeable but - # (i1, i2-1) is not; e.g., 'A' in the following diagram: - # - # i1 - # - # 0123456 - # 0 **|*|** - # 1 **|*|?? - # i2 2 --+-+xx - # 3 **|xxAx - # 4 --+xxxx - # 5 *xxxxxx - # - # The block ending at (i1-1,i2-1) has just been recorded. - # Now find the height of the conflict rectangle at column - # i1 and fill it in: - if i2 - 1 == 1 or not block.is_mergeable(i1, 1): - break - else: - i2 = find_first_false( - lambda i: block.is_mergeable(i1, i), - 2, i2 - 1, - ) - - -class MergeFrontier(object): - """Represents the merge frontier within a Block. - - A MergeFrontier is represented by a list of SubBlocks, each of - which is thought to be completely mergeable. The list is kept in - normalized form: - - * Only non-empty blocks are retained - - * Blocks are sorted from bottom left to upper right - - * No redundant blocks - - """ - - @staticmethod - def map_known_frontier(block): - """Return the MergeFrontier describing existing successful merges in block. - - The return value only includes the part that is fully outlined - and whose outline consists of rectangles reaching back to - (0,0). - - A blocked commit is *not* considered to be within the - frontier, even if a merge is registered for it. Such merges - must be explicitly unblocked.""" - - # FIXME: This algorithm can take combinatorial time, for - # example if there is a big block of merges that is a dead - # end: - # - # +++++++ - # +?+++++ - # +?+++++ - # +?+++++ - # +?*++++ - # - # The problem is that the algorithm will explore all of the - # ways of getting to commit *, and the number of paths grows - # like a binomial coefficient. The solution would be to - # remember dead-ends and reject any curves that visit a point - # to the right of a dead-end. - # - # For now we don't intend to allow a situation like this to be - # created, so we ignore the problem. - - # A list (i1, i2, down) of points in the path so far. down is - # True iff the attempted step following this one was - # downwards. - path = [] - - def create_frontier(path): - blocks = [] - for ((i1old, i2old, downold), (i1new, i2new, downnew)) in iter_neighbors(path): - if downold is True and downnew is False: - blocks.append(block[:i1new + 1, :i2new + 1]) - return MergeFrontier(block, blocks) - - # Loop invariants: - # - # * path is a valid path - # - # * (i1, i2) is in block but it not yet added to path - # - # * down is True if a step downwards from (i1, i2) has not yet - # been attempted - (i1, i2) = (block.len1 - 1, 0) - down = True - while True: - if down: - if i2 == block.len2 - 1: - # Hit edge of block; can't move down: - down = False - elif (i1, i2 + 1) in block and not block.is_blocked(i1, i2 + 1): - # Can move down - path.append((i1, i2, True)) - i2 += 1 - else: - # Can't move down. - down = False - else: - if i1 == 0: - # Success! - path.append((i1, i2, False)) - return create_frontier(path) - elif (i1 - 1, i2) in block and not block.is_blocked(i1 - 1, i2): - # Can move left - path.append((i1, i2, False)) - down = True - i1 -= 1 - else: - # There's no way to go forward; backtrack until we - # find a place where we can still try going left: - while True: - try: - (i1, i2, down) = path.pop() - except IndexError: - # This shouldn't happen because, in the - # worst case, there is a valid path across - # the top edge of the merge diagram. - raise RuntimeError('Block is improperly formed!') - if down: - down = False - break - - @staticmethod - def compute_by_bisection(block): - """Return a MergeFrontier instance for block. - - Compute the blocks making up the boundary using bisection. See - find_frontier_blocks() for more information. - - """ - - return MergeFrontier(block, list(find_frontier_blocks(block))) - - def __init__(self, block, blocks=None): - self.block = block - self.blocks = self._normalized_blocks(blocks or []) - - def __iter__(self): - """Iterate over blocks from bottom left to upper right.""" - - return iter(self.blocks) - - def __bool__(self): - """Return True iff this frontier has no completed parts.""" - - return bool(self.blocks) - - def __nonzero__(self): - """Return True iff this frontier has no completed parts.""" - - return bool(self.blocks) - - def is_complete(self): - """Return True iff the frontier covers the whole block.""" - - return ( - len(self.blocks) == 1 - and self.blocks[0].len1 == self.block.len1 - and self.blocks[0].len2 == self.block.len2 - ) - - # Additional codes used in the 2D array returned from create_diagram() - FRONTIER_WITHIN = 0x10 - FRONTIER_RIGHT_EDGE = 0x20 - FRONTIER_BOTTOM_EDGE = 0x40 - FRONTIER_MASK = 0x70 - - @classmethod - def default_formatter(cls, node, legend=None): - def color(node, within): - if within: - return AnsiColor.B_GREEN + node + AnsiColor.END - else: - return AnsiColor.B_RED + node + AnsiColor.END - - if legend is None: - legend = ['?', '*', '.', '#', '@', '-', '|', '+'] - merge = node & Block.MERGE_MASK - within = merge == Block.MERGE_MANUAL or (node & cls.FRONTIER_WITHIN) - skip = [Block.MERGE_MANUAL, Block.MERGE_BLOCKED, Block.MERGE_UNBLOCKED] - if merge not in skip: - vertex = (cls.FRONTIER_BOTTOM_EDGE | cls.FRONTIER_RIGHT_EDGE) - edge_status = node & vertex - if edge_status == vertex: - return color(legend[-1], within) - elif edge_status == cls.FRONTIER_RIGHT_EDGE: - return color(legend[-2], within) - elif edge_status == cls.FRONTIER_BOTTOM_EDGE: - return color(legend[-3], within) - return color(legend[merge], within) - - def create_diagram(self): - """Generate a diagram of this frontier. - - The returned diagram is a nested list of integers forming a 2D array, - representing the merge frontier embedded in the diagram of commits - returned from Block.create_diagram(). - - At each node in the returned diagram is an integer whose value is a - bitwise-or of existing MERGE_* constant from Block.create_diagram() - and zero or more of the FRONTIER_* constants defined in this class.""" - - diagram = self.block.create_diagram() - - try: - next_block = self.blocks[0] - except IndexError: - next_block = None - - diagram[0][-1] |= self.FRONTIER_BOTTOM_EDGE - for i2 in range(1, self.block.len2): - if next_block is None or i2 >= next_block.len2: - diagram[0][i2] |= self.FRONTIER_RIGHT_EDGE - - prev_block = None - for n in range(len(self.blocks)): - block = self.blocks[n] - try: - next_block = self.blocks[n + 1] - except IndexError: - next_block = None - - for i1 in range(block.len1): - for i2 in range(block.len2): - v = self.FRONTIER_WITHIN - if i1 == block.len1 - 1 and ( - next_block is None or i2 >= next_block.len2 - ): - v |= self.FRONTIER_RIGHT_EDGE - if i2 == block.len2 - 1 and ( - prev_block is None or i1 >= prev_block.len1 - ): - v |= self.FRONTIER_BOTTOM_EDGE - diagram[i1][i2] |= v - prev_block = block - - try: - prev_block = self.blocks[-1] - except IndexError: - prev_block = None - - for i1 in range(1, self.block.len1): - if prev_block is None or i1 >= prev_block.len1: - diagram[i1][0] |= self.FRONTIER_BOTTOM_EDGE - diagram[-1][0] |= self.FRONTIER_RIGHT_EDGE - - return diagram - - def format_diagram(self, formatter=None, diagram=None): - if formatter is None: - formatter = self.default_formatter - if diagram is None: - diagram = self.create_diagram() - return [ - [formatter(diagram[i1][i2]) for i2 in range(self.block.len2)] - for i1 in range(self.block.len1)] - - def write(self, f): - """Write this frontier to file-like object f.""" - diagram = self.format_diagram() - for i2 in range(self.block.len2): - for i1 in range(self.block.len1): - f.write(diagram[i1][i2]) - f.write('\n') - - def write_html(self, f, name, cssfile='imerge.css', abbrev_sha1=7): - class_map = { - Block.MERGE_UNKNOWN: 'merge_unknown', - Block.MERGE_MANUAL: 'merge_manual', - Block.MERGE_AUTOMATIC: 'merge_automatic', - Block.MERGE_BLOCKED: 'merge_blocked', - Block.MERGE_UNBLOCKED: 'merge_unblocked', - self.FRONTIER_WITHIN: 'frontier_within', - self.FRONTIER_RIGHT_EDGE: 'frontier_right_edge', - self.FRONTIER_BOTTOM_EDGE: 'frontier_bottom_edge', - } - - def map_to_classes(i1, i2, node): - merge = node & Block.MERGE_MASK - ret = [class_map[merge]] - for bit in [self.FRONTIER_WITHIN, self.FRONTIER_RIGHT_EDGE, - self.FRONTIER_BOTTOM_EDGE]: - if node & bit: - ret.append(class_map[bit]) - if not (node & self.FRONTIER_WITHIN): - ret.append('frontier_without') - elif (node & Block.MERGE_MASK) == Block.MERGE_UNKNOWN: - ret.append('merge_skipped') - if i1 == 0 or i2 == 0: - ret.append('merge_initial') - if i1 == 0: - ret.append('col_left') - if i1 == self.block.len1 - 1: - ret.append('col_right') - if i2 == 0: - ret.append('row_top') - if i2 == self.block.len2 - 1: - ret.append('row_bottom') - return ret - - f.write("""\ - - -git-imerge: %s - - - - -""" % (name, cssfile)) - - diagram = self.create_diagram() - - f.write(' \n') - f.write(' \n') - - for i2 in range(self.block.len2): - f.write(' \n') - f.write(' \n') - f.write('
 \n') - for i1 in range(self.block.len1): - f.write(' %d-*\n' % (i1,)) - f.write('
*-%d\n' % (i2,)) - for i1 in range(self.block.len1): - classes = map_to_classes(i1, i2, diagram[i1][i2]) - record = self.block.get_value(i1, i2) - sha1 = record.sha1 or '' - td_id = record.sha1 and ' id="%s"' % (record.sha1) or '' - td_class = classes and ' class="' + ' '.join(classes) + '"' or '' - f.write(' %.*s\n' % ( - td_id, td_class, abbrev_sha1, sha1)) - f.write('
\n\n\n') - - @staticmethod - def _normalized_blocks(blocks): - """Return a normalized list of blocks from the argument. - - * Remove empty blocks. - - * Remove redundant blocks. - - * Sort the blocks according to their len1 members. - - """ - - def contains(block1, block2): - """Return true if block1 contains block2.""" - - return block1.len1 >= block2.len1 and block1.len2 >= block2.len2 - - blocks = sorted(blocks, key=lambda block: block.len1) - ret = [] - - for block in blocks: - if block.len1 == 0 or block.len2 == 0: - continue - while True: - if not ret: - ret.append(block) - break - - last = ret[-1] - if contains(last, block): - break - elif contains(block, last): - ret.pop() - else: - ret.append(block) - break - - return ret - - def remove_failure(self, i1, i2): - """Refine the merge frontier given that the specified merge fails.""" - - newblocks = [] - shrunk_block = False - - for block in self.blocks: - if i1 < block.len1 and i2 < block.len2: - if i1 > 1: - newblocks.append(block[:i1, :]) - if i2 > 1: - newblocks.append(block[:, :i2]) - shrunk_block = True - else: - newblocks.append(block) - - if shrunk_block: - self.blocks = self._normalized_blocks(newblocks) - - def partition(self, block): - """Return two MergeFrontier instances partitioned by block. - - Return (frontier1, frontier2), where each frontier is limited - to each side of the argument. - - block must be contained in this MergeFrontier and already be - outlined.""" - - # Remember that the new blocks have to include the outlined - # edge of the partitioning block to satisfy the invariant that - # the left and upper edge of a block has to be known. - - left = [] - right = [] - for b in self.blocks: - if b.len1 == block.len1 and b.len2 == block.len2: - # That's the block we're partitioning on; just skip it. - pass - elif b.len1 < block.len1 and b.len2 > block.len2: - left.append(b[:, block.len2 - 1:]) - elif b.len1 > block.len1 and b.len2 < block.len2: - right.append(b[block.len1 - 1:, :]) - else: - raise ValueError( - 'MergeFrontier partitioned with inappropriate block' - ) - return ( - MergeFrontier(self.block[:block.len1, block.len2 - 1:], left), - MergeFrontier(self.block[block.len1 - 1:, :block.len2], right), - ) - - def iter_blocker_blocks(self): - """Iterate over the blocks on the far side of this frontier. - - This must only be called for an outlined frontier.""" - - if not self: - yield self.block - return - - blockruns = [] - if self.blocks[0].len2 < self.block.len2: - blockruns.append([self.block[0, :]]) - blockruns.append(self) - if self.blocks[-1].len1 < self.block.len1: - blockruns.append([self.block[:, 0]]) - - for block1, block2 in iter_neighbors(itertools.chain(*blockruns)): - yield self.block[block1.len1 - 1:block2.len1, block2.len2 - 1: block1.len2] - - def get_affected_blocker_block(self, i1, i2): - """Return the blocker block that a successful merge (i1,i2) would unblock. - - If there is no such block, raise NotABlockingCommitError.""" - - for block in self.iter_blocker_blocks(): - try: - (block_i1, block_i2) = block.convert_original_indexes(i1, i2) - except IndexError: - pass - else: - if (block_i1, block_i2) == (1, 1): - # That's the one we need to improve this block: - return block - else: - # An index pair can only be in a single blocker - # block, which we've already found: - raise NotABlockingCommitError( - 'Commit %d-%d was not blocking the frontier.' - % self.block.get_original_indexes(i1, i2) - ) - else: - raise NotABlockingCommitError( - 'Commit %d-%d was not on the frontier.' - % self.block.get_original_indexes(i1, i2) - ) - - def auto_expand(self): - """Try pushing out one of the blocks on this frontier. - - Raise BlockCompleteError if the whole block has already been - solved. Raise FrontierBlockedError if the frontier is blocked - everywhere. This method does *not* update self; if it returns - successfully you should recompute the frontier from - scratch.""" - - blocks = list(self.iter_blocker_blocks()) - if not blocks: - raise BlockCompleteError('The block is already complete') - - # Try blocks from left to right: - blocks.sort(key=lambda block: block.get_original_indexes(0, 0)) - - for block in blocks: - if block.auto_expand_frontier(): - return - else: - # None of the blocks could be expanded. Suggest that the - # caller do a manual merge of the commit that is blocking - # the leftmost blocker block. - i1, i2 = blocks[0].get_original_indexes(1, 1) - raise FrontierBlockedError( - 'Conflict; suggest manual merge of %d-%d' % (i1, i2), - i1, i2 - ) - - -class NoManualMergeError(Exception): - pass - - -class ManualMergeUnusableError(Exception): - def __init__(self, msg, commit): - Exception.__init__(self, 'Commit %s is not usable; %s' % (commit, msg)) - self.commit = commit - - -class CommitNotFoundError(Exception): - def __init__(self, commit): - Exception.__init__( - self, - 'Commit %s was not found among the known merge commits' % (commit,), - ) - self.commit = commit - - -class Block(object): - """A rectangular range of commits, indexed by (i1,i2). - - The commits block[0,1:] and block[1:,0] are always all known. - block[0,0] may or may not be known; it is usually unneeded (except - maybe implicitly). - - Members: - - name -- the name of the imerge of which this block is part. - - len1, len2 -- the dimensions of the block. - - """ - - def __init__(self, git, name, len1, len2): - self.git = git - self.name = name - self.len1 = len1 - self.len2 = len2 - - def get_merge_state(self): - """Return the MergeState instance containing this Block.""" - - raise NotImplementedError() - - def get_area(self): - """Return the area of this block, ignoring the known edges.""" - - return (self.len1 - 1) * (self.len2 - 1) - - def _check_indexes(self, i1, i2): - if not (0 <= i1 < self.len1): - raise IndexError('first index (%s) is out of range 0:%d' % (i1, self.len1,)) - if not (0 <= i2 < self.len2): - raise IndexError('second index (%s) is out of range 0:%d' % (i2, self.len2,)) - - def _normalize_indexes(self, index): - """Return a pair of non-negative integers (i1, i2).""" - - try: - (i1, i2) = index - except TypeError: - raise IndexError('Block indexing requires exactly two indexes') - - if i1 < 0: - i1 += self.len1 - if i2 < 0: - i2 += self.len2 - - self._check_indexes(i1, i2) - return (i1, i2) - - def get_original_indexes(self, i1, i2): - """Return the original indexes corresponding to (i1,i2) in this block. - - This function supports negative indexes.""" - - return self._normalize_indexes((i1, i2)) - - def convert_original_indexes(self, i1, i2): - """Return the indexes in this block corresponding to original indexes (i1,i2). - - raise IndexError if they are not within this block. This - method does not support negative indices.""" - - return (i1, i2) - - def _set_value(self, i1, i2, value): - """Set the MergeRecord for integer indexes (i1, i2). - - i1 and i2 must be non-negative.""" - - raise NotImplementedError() - - def get_value(self, i1, i2): - """Return the MergeRecord for integer indexes (i1, i2). - - i1 and i2 must be non-negative.""" - - raise NotImplementedError() - - def __getitem__(self, index): - """Return the MergeRecord at (i1, i2) (requires two indexes). - - If i1 and i2 are integers but the merge is unknown, return - None. If either index is a slice, return a SubBlock.""" - - try: - (i1, i2) = index - except TypeError: - raise IndexError('Block indexing requires exactly two indexes') - if isinstance(i1, slice) or isinstance(i2, slice): - return SubBlock(self, i1, i2) - else: - return self.get_value(*self._normalize_indexes((i1, i2))) - - def __contains__(self, index): - return self[index].is_known() - - def is_blocked(self, i1, i2): - """Return True iff the specified commit is blocked.""" - - (i1, i2) = self._normalize_indexes((i1, i2)) - return self[i1, i2].is_blocked() - - def is_mergeable(self, i1, i2): - """Determine whether (i1,i2) can be merged automatically. - - If we already have a merge record for (i1,i2), return True. - Otherwise, attempt a merge (discarding the result).""" - - (i1, i2) = self._normalize_indexes((i1, i2)) - if (i1, i2) in self: - return True - else: - sys.stderr.write( - 'Attempting automerge of %d-%d...' % self.get_original_indexes(i1, i2) - ) - try: - self.git.automerge(self[i1, 0].sha1, self[0, i2].sha1) - sys.stderr.write('success.\n') - return True - except AutomaticMergeFailed: - sys.stderr.write('failure.\n') - return False - - def auto_outline(self): - """Complete the outline of this Block. - - raise UnexpectedMergeFailure if automerging fails.""" - - # Check that all of the merges go through before recording any - # of them permanently. - merges = [] - - def do_merge(i1, commit1, i2, commit2, msg='Autofilling %d-%d...', record=True): - if (i1, i2) in self: - return self[i1, i2].sha1 - (i1orig, i2orig) = self.get_original_indexes(i1, i2) - sys.stderr.write(msg % (i1orig, i2orig)) - logmsg = 'imerge \'%s\': automatic merge %d-%d' % (self.name, i1orig, i2orig) - try: - merge = self.git.automerge(commit1, commit2, msg=logmsg) - sys.stderr.write('success.\n') - except AutomaticMergeFailed as e: - sys.stderr.write('unexpected conflict. Backtracking...\n') - raise UnexpectedMergeFailure(str(e), i1, i2) - if record: - merges.append((i1, i2, merge)) - return merge - - i2 = self.len2 - 1 - left = self[0, i2].sha1 - for i1 in range(1, self.len1 - 1): - left = do_merge(i1, self[i1, 0].sha1, i2, left) - - i1 = self.len1 - 1 - above = self[i1, 0].sha1 - for i2 in range(1, self.len2 - 1): - above = do_merge(i1, above, i2, self[0, i2].sha1) - - i1, i2 = self.len1 - 1, self.len2 - 1 - if i1 > 1 and i2 > 1: - # We will compare two ways of doing the final "vertex" merge: - # as a continuation of the bottom edge, or as a continuation - # of the right edge. We only accept it if both approaches - # succeed and give identical trees. - vertex_v1 = do_merge( - i1, self[i1, 0].sha1, i2, left, - msg='Autofilling %d-%d (first way)...', - record=False, - ) - vertex_v2 = do_merge( - i1, above, i2, self[0, i2].sha1, - msg='Autofilling %d-%d (second way)...', - record=False, - ) - if self.git.get_tree(vertex_v1) == self.git.get_tree(vertex_v2): - sys.stderr.write( - 'The two ways of autofilling %d-%d agree.\n' - % self.get_original_indexes(i1, i2) - ) - # Everything is OK. Now reparent the actual vertex merge to - # have above and left as its parents: - merges.append( - (i1, i2, self.git.reparent(vertex_v1, [above, left])) - ) - else: - sys.stderr.write( - 'The two ways of autofilling %d-%d do not agree. Backtracking...\n' - % self.get_original_indexes(i1, i2) - ) - raise UnexpectedMergeFailure('Inconsistent vertex merges', i1, i2) - else: - do_merge( - i1, above, i2, left, - msg='Autofilling %d-%d...', - ) - - # Done! Now we can record the results: - sys.stderr.write('Recording autofilled block %s.\n' % (self,)) - for (i1, i2, merge) in merges: - self[i1, i2].record_merge(merge, MergeRecord.NEW_AUTO) - - def auto_fill_micromerge(self): - """Try to fill the very first micromerge in this block. - - Return True iff the attempt was successful.""" - - assert (1, 1) not in self - if self.len1 <= 1 or self.len2 <= 1 or self.is_blocked(1, 1): - return False - - i1, i2 = 1, 1 - (i1orig, i2orig) = self.get_original_indexes(i1, i2) - sys.stderr.write('Attempting to merge %d-%d...' % (i1orig, i2orig)) - logmsg = 'imerge \'%s\': automatic merge %d-%d' % (self.name, i1orig, i2orig) - try: - merge = self.git.automerge( - self[i1, i2 - 1].sha1, - self[i1 - 1, i2].sha1, - msg=logmsg, - ) - sys.stderr.write('success.\n') - except AutomaticMergeFailed: - sys.stderr.write('conflict.\n') - self[i1, i2].record_blocked(True) - return False - else: - self[i1, i2].record_merge(merge, MergeRecord.NEW_AUTO) - return True - - def auto_outline_frontier(self, merge_frontier=None): - """Try to outline the merge frontier of this block. - - Return True iff some progress was made.""" - - if merge_frontier is None: - merge_frontier = MergeFrontier.compute_by_bisection(self) - - if not merge_frontier: - # Nothing to do. - return False - - best_block = max(merge_frontier, key=lambda block: block.get_original_indexes(0, 0)) - - try: - best_block.auto_outline() - except UnexpectedMergeFailure as e: - # One of the merges that we expected to succeed in - # fact failed. - merge_frontier.remove_failure(e.i1, e.i2) - return self.auto_outline_frontier(merge_frontier) - else: - f1, f2 = merge_frontier.partition(best_block) - if f1: - f1.block.auto_outline_frontier(f1) - if f2: - f2.block.auto_outline_frontier(f2) - return True - - def auto_expand_frontier(self): - merge_state = self.get_merge_state() - if merge_state.manual: - return False - elif merge_state.goal == 'full': - return self.auto_fill_micromerge() - else: - return self.auto_outline_frontier() - - # The codes in the 2D array returned from create_diagram() - MERGE_UNKNOWN = 0 - MERGE_MANUAL = 1 - MERGE_AUTOMATIC = 2 - MERGE_BLOCKED = 3 - MERGE_UNBLOCKED = 4 - MERGE_MASK = 7 - - # A map {(is_known(), manual, is_blocked()) : integer constant} - MergeState = { - (False, False, False): MERGE_UNKNOWN, - (False, False, True): MERGE_BLOCKED, - (True, False, True): MERGE_UNBLOCKED, - (True, True, True): MERGE_UNBLOCKED, - (True, False, False): MERGE_AUTOMATIC, - (True, True, False): MERGE_MANUAL, - } - - def create_diagram(self): - """Generate a diagram of this Block. - - The returned diagram, is a nested list of integers forming a 2D array, - where the integer at diagram[i1][i2] is one of MERGE_UNKNOWN, - MERGE_MANUAL, MERGE_AUTOMATIC, MERGE_BLOCKED, or MERGE_UNBLOCKED, - representing the state of the commit at (i1, i2).""" - - diagram = [[None for i2 in range(self.len2)] for i1 in range(self.len1)] - - for i2 in range(self.len2): - for i1 in range(self.len1): - rec = self.get_value(i1, i2) - c = self.MergeState[ - rec.is_known(), rec.is_manual(), rec.is_blocked()] - diagram[i1][i2] = c - - return diagram - - def format_diagram(self, legend=None, diagram=None): - if legend is None: - legend = [ - AnsiColor.D_GRAY + '?' + AnsiColor.END, - AnsiColor.B_GREEN + '*' + AnsiColor.END, - AnsiColor.B_GREEN + '.' + AnsiColor.END, - AnsiColor.B_RED + '#' + AnsiColor.END, - AnsiColor.B_YELLOW + '@' + AnsiColor.END, - ] - if diagram is None: - diagram = self.create_diagram() - return [ - [legend[diagram[i1][i2]] for i2 in range(self.len2)] - for i1 in range(self.len1)] - - def write(self, f, legend=None, sep='', linesep='\n'): - diagram = self.format_diagram(legend) - for i2 in range(self.len2): - f.write(sep.join(diagram[i1][i2] for i1 in range(self.len1)) + linesep) - - def writeppm(self, f): - f.write('P3\n') - f.write('%d %d 255\n' % (self.len1, self.len2,)) - legend = ['127 127 0', '0 255 0', '0 127 0', '255 0 0', '127 0 0'] - self.write(f, legend, sep=' ') - - -class SubBlock(Block): - @staticmethod - def _convert_to_slice(i, len): - """Return (start, len) for the specified index. - - i may be an integer or a slice with step equal to 1.""" - - if isinstance(i, int): - if i < 0: - i += len - i = slice(i, i + 1) - elif isinstance(i, slice): - if i.step is not None and i.step != 1: - raise ValueError('Index has a non-zero step size') - else: - raise ValueError('Index cannot be converted to a slice') - - (start, stop, step) = i.indices(len) - return (start, stop - start) - - def __init__(self, block, slice1, slice2): - (start1, len1) = self._convert_to_slice(slice1, block.len1) - (start2, len2) = self._convert_to_slice(slice2, block.len2) - Block.__init__(self, block.git, block.name, len1, len2) - if isinstance(block, SubBlock): - # Peel away one level of indirection: - self._merge_state = block._merge_state - self._start1 = start1 + block._start1 - self._start2 = start2 + block._start2 - else: - assert(isinstance(block, MergeState)) - self._merge_state = block - self._start1 = start1 - self._start2 = start2 - - def get_merge_state(self): - return self._merge_state - - def get_original_indexes(self, i1, i2): - i1, i2 = self._normalize_indexes((i1, i2)) - return self._merge_state.get_original_indexes( - i1 + self._start1, - i2 + self._start2, - ) - - def convert_original_indexes(self, i1, i2): - (i1, i2) = self._merge_state.convert_original_indexes(i1, i2) - if not ( - self._start1 <= i1 < self._start1 + self.len1 - and self._start2 <= i2 < self._start2 + self.len2 - ): - raise IndexError('Indexes are not within block') - return (i1 - self._start1, i2 - self._start2) - - def _set_value(self, i1, i2, sha1, flags): - self._check_indexes(i1, i2) - self._merge_state._set_value( - i1 + self._start1, - i2 + self._start2, - sha1, flags, - ) - - def get_value(self, i1, i2): - self._check_indexes(i1, i2) - return self._merge_state.get_value(i1 + self._start1, i2 + self._start2) - - def __str__(self): - return '%s[%d:%d,%d:%d]' % ( - self._merge_state, - self._start1, self._start1 + self.len1, - self._start2, self._start2 + self.len2, - ) - - -class MissingMergeFailure(Failure): - def __init__(self, i1, i2): - Failure.__init__(self, 'Merge %d-%d is not yet done' % (i1, i2)) - self.i1 = i1 - self.i2 = i2 - - -class MergeState(Block): - SOURCE_TABLE = { - 'auto': MergeRecord.SAVED_AUTO, - 'manual': MergeRecord.SAVED_MANUAL, - } - - @staticmethod - def get_scratch_refname(name): - return 'refs/heads/imerge/%s' % (name,) - - @staticmethod - def _check_no_merges(git, commits): - multiparent_commits = [ - commit - for commit in commits - if len(git.get_commit_parents(commit)) > 1 - ] - if multiparent_commits: - raise Failure( - 'The following commits on the to-be-merged branch are merge commits:\n' - ' %s\n' - '--goal=\'rebase\' is not yet supported for branches that include merges.\n' - % ('\n '.join(multiparent_commits),) - ) - - @staticmethod - def initialize( - git, name, merge_base, - tip1, commits1, - tip2, commits2, - goal=DEFAULT_GOAL, goalopts=None, - manual=False, branch=None, - ): - """Create and return a new MergeState object.""" - - git.verify_imerge_name_available(name) - if branch: - git.check_branch_name_format(branch) - else: - branch = name - - if goal == 'rebase': - MergeState._check_no_merges(git, commits2) - - return MergeState( - git, name, merge_base, - tip1, commits1, - tip2, commits2, - MergeRecord.NEW_MANUAL, - goal=goal, goalopts=goalopts, - manual=manual, - branch=branch, - ) - - @staticmethod - def read(git, name): - (state, merges) = git.read_imerge_state(name) - - # Translate sources from strings into MergeRecord constants - # SAVED_AUTO or SAVED_MANUAL: - merges = dict(( - ((i1, i2), (sha1, MergeState.SOURCE_TABLE[source])) - for ((i1, i2), (sha1, source)) in merges.items() - )) - - blockers = state.get('blockers', []) - - # Find merge_base, commits1, and commits2: - (merge_base, source) = merges.pop((0, 0)) - if source != MergeRecord.SAVED_MANUAL: - raise Failure('Merge base should be manual!') - commits1 = [] - for i1 in itertools.count(1): - try: - (sha1, source) = merges.pop((i1, 0)) - if source != MergeRecord.SAVED_MANUAL: - raise Failure('Merge %d-0 should be manual!' % (i1,)) - commits1.append(sha1) - except KeyError: - break - - commits2 = [] - for i2 in itertools.count(1): - try: - (sha1, source) = merges.pop((0, i2)) - if source != MergeRecord.SAVED_MANUAL: - raise Failure('Merge (0,%d) should be manual!' % (i2,)) - commits2.append(sha1) - except KeyError: - break - - tip1 = state.get('tip1', commits1[-1]) - tip2 = state.get('tip2', commits2[-1]) - - goal = state['goal'] - if goal not in ALLOWED_GOALS: - raise Failure('Goal %r, read from state, is not recognized.' % (goal,)) - - goalopts = state['goalopts'] - - manual = state['manual'] - branch = state.get('branch', name) - - state = MergeState( - git, name, merge_base, - tip1, commits1, - tip2, commits2, - MergeRecord.SAVED_MANUAL, - goal=goal, goalopts=goalopts, - manual=manual, - branch=branch, - ) - - # Now write the rest of the merges to state: - for ((i1, i2), (sha1, source)) in merges.items(): - if i1 == 0 and i2 >= state.len2: - raise Failure('Merge 0-%d is missing!' % (state.len2,)) - if i1 >= state.len1 and i2 == 0: - raise Failure('Merge %d-0 is missing!' % (state.len1,)) - if i1 >= state.len1 or i2 >= state.len2: - raise Failure( - 'Merge %d-%d is out of range [0:%d,0:%d]' - % (i1, i2, state.len1, state.len2) - ) - state[i1, i2].record_merge(sha1, source) - - # Record any blockers: - for (i1, i2) in blockers: - state[i1, i2].record_blocked(True) - - return state - - @staticmethod - def remove(git, name): - # If HEAD is the scratch refname, abort any in-progress - # commits and detach HEAD: - scratch_refname = MergeState.get_scratch_refname(name) - if git.get_head_refname() == scratch_refname: - try: - git.abort_merge() - except CalledProcessError: - pass - # Detach head so that we can delete scratch_refname: - git.detach('Detach HEAD from %s' % (scratch_refname,)) - - # Delete the scratch refname: - git.delete_ref( - scratch_refname, 'imerge %s: remove scratch reference' % (name,), - ) - - # Remove any references referring to intermediate merges: - git.delete_imerge_refs(name) - - # If this merge was the default, unset the default: - if git.get_default_imerge_name() == name: - git.set_default_imerge_name(None) - - def __init__( - self, git, name, merge_base, - tip1, commits1, - tip2, commits2, - source, - goal=DEFAULT_GOAL, goalopts=None, - manual=False, - branch=None, - ): - Block.__init__(self, git, name, len(commits1) + 1, len(commits2) + 1) - self.tip1 = tip1 - self.tip2 = tip2 - self.goal = goal - self.goalopts = goalopts - self.manual = bool(manual) - self.branch = branch or name - - # A simulated 2D array. Values are None or MergeRecord instances. - self._data = [[None] * self.len2 for i1 in range(self.len1)] - - self.get_value(0, 0).record_merge(merge_base, source) - for (i1, commit) in enumerate(commits1, 1): - self.get_value(i1, 0).record_merge(commit, source) - for (i2, commit) in enumerate(commits2, 1): - self.get_value(0, i2).record_merge(commit, source) - - def get_merge_state(self): - return self - - def set_goal(self, goal): - if goal not in ALLOWED_GOALS: - raise ValueError('%r is not an allowed goal' % (goal,)) - - if goal == 'rebase': - self._check_no_merges( - self.git, - [self[0, i2].sha1 for i2 in range(1, self.len2)], - ) - - self.goal = goal - - def _set_value(self, i1, i2, value): - self._data[i1][i2] = value - - def get_value(self, i1, i2): - value = self._data[i1][i2] - # Missing values spring to life on first access: - if value is None: - value = MergeRecord() - self._data[i1][i2] = value - return value - - def __contains__(self, index): - # Avoid creating new MergeRecord objects here. - (i1, i2) = self._normalize_indexes(index) - value = self._data[i1][i2] - return (value is not None) and value.is_known() - - def auto_complete_frontier(self): - """Complete the frontier using automerges. - - If progress is blocked before the frontier is complete, raise - a FrontierBlockedError. Save the state as progress is - made.""" - - progress_made = False - try: - while True: - frontier = MergeFrontier.map_known_frontier(self) - frontier.auto_expand() - self.save() - progress_made = True - except BlockCompleteError: - return - except FrontierBlockedError as e: - self.save() - if not progress_made: - # Adjust the error message: - raise FrontierBlockedError( - 'No progress was possible; suggest manual merge of %d-%d' - % (e.i1, e.i2), - e.i1, e.i2, - ) - else: - raise - - def find_index(self, commit): - """Return (i1,i2) for the specified commit. - - Raise CommitNotFoundError if it is not known.""" - - for i2 in range(0, self.len2): - for i1 in range(0, self.len1): - if (i1, i2) in self: - record = self[i1, i2] - if record.sha1 == commit: - return (i1, i2) - raise CommitNotFoundError(commit) - - def request_user_merge(self, i1, i2): - """Prepare the working tree for the user to do a manual merge. - - It is assumed that the merges above and to the left of (i1, i2) - are already done.""" - - above = self[i1, i2 - 1] - left = self[i1 - 1, i2] - if not above.is_known() or not left.is_known(): - raise RuntimeError('The parents of merge %d-%d are not ready' % (i1, i2)) - refname = MergeState.get_scratch_refname(self.name) - self.git.update_ref( - refname, above.sha1, - 'imerge %r: Prepare merge %d-%d' % (self.name, i1, i2,), - ) - self.git.checkout(refname) - logmsg = 'imerge \'%s\': manual merge %d-%d' % (self.name, i1, i2) - try: - self.git.manualmerge(left.sha1, logmsg) - except CalledProcessError: - # We expect an error (otherwise we would have automerged!) - pass - sys.stderr.write( - '\n' - 'Original first commit:\n' - ) - self.git.summarize_commit(self[i1, 0].sha1) - sys.stderr.write( - '\n' - 'Original second commit:\n' - ) - self.git.summarize_commit(self[0, i2].sha1) - sys.stderr.write( - '\n' - 'There was a conflict merging commit %d-%d, shown above.\n' - 'Please resolve the conflict, commit the result, then type\n' - '\n' - ' git-imerge continue\n' - % (i1, i2) - ) - - def incorporate_manual_merge(self, commit): - """Record commit as a manual merge of its parents. - - Return the indexes (i1,i2) where it was recorded. If the - commit is not usable for some reason, raise - ManualMergeUnusableError.""" - - parents = self.git.get_commit_parents(commit) - if len(parents) < 2: - raise ManualMergeUnusableError('it is not a merge', commit) - if len(parents) > 2: - raise ManualMergeUnusableError('it is an octopus merge', commit) - # Find the parents among our contents... - try: - (i1first, i2first) = self.find_index(parents[0]) - (i1second, i2second) = self.find_index(parents[1]) - except CommitNotFoundError: - raise ManualMergeUnusableError( - 'its parents are not known merge commits', commit, - ) - swapped = False - if i1first < i1second: - # Swap parents to make the parent from above the first parent: - (i1first, i2first, i1second, i2second) = (i1second, i2second, i1first, i2first) - swapped = True - if i1first != i1second + 1 or i2first != i2second - 1: - raise ManualMergeUnusableError( - 'it is not a pairwise merge of adjacent parents', commit, - ) - if swapped: - # Create a new merge with the parents in the conventional order: - commit = self.git.reparent(commit, [parents[1], parents[0]]) - - i1, i2 = i1first, i2second - self[i1, i2].record_merge(commit, MergeRecord.NEW_MANUAL) - return (i1, i2) - - def incorporate_user_merge(self, edit_log_msg=None): - """If the user has done a merge for us, incorporate the results. - - If the scratch reference refs/heads/imerge/NAME exists and is - checked out, first check if there are staged changes that can - be committed. Then try to incorporate the current commit into - this MergeState, delete the reference, and return (i1,i2) - corresponding to the merge. If the scratch reference does not - exist, raise NoManualMergeError(). If the scratch reference - exists but cannot be used, raise a ManualMergeUnusableError. - If there are unstaged changes in the working tree, emit an - error message and raise UncleanWorkTreeError. - - """ - - refname = MergeState.get_scratch_refname(self.name) - - try: - commit = self.git.get_commit_sha1(refname) - except ValueError: - raise NoManualMergeError('Reference %s does not exist.' % (refname,)) - - head_name = self.git.get_head_refname() - if head_name is None: - raise NoManualMergeError('HEAD is currently detached.') - elif head_name != refname: - # This should not usually happen. The scratch reference - # exists, but it is not current. Perhaps the user gave up on - # an attempted merge then switched to another branch. We want - # to delete refname, but only if it doesn't contain any - # content that we don't already know. - try: - self.find_index(commit) - except CommitNotFoundError: - # It points to a commit that we don't have in our records. - raise Failure( - 'The scratch reference, %(refname)s, already exists but is not\n' - 'checked out. If it points to a merge commit that you would like\n' - 'to use, please check it out using\n' - '\n' - ' git checkout %(refname)s\n' - '\n' - 'and then try to continue again. If it points to a commit that is\n' - 'unneeded, then please delete the reference using\n' - '\n' - ' git update-ref -d %(refname)s\n' - '\n' - 'and then continue.' - % dict(refname=refname) - ) - else: - # It points to a commit that is already recorded. We can - # delete it without losing any information. - self.git.delete_ref( - refname, - 'imerge %r: Remove obsolete scratch reference' % (self.name,), - ) - sys.stderr.write( - '%s did not point to a new merge; it has been deleted.\n' - % (refname,) - ) - raise NoManualMergeError( - 'Reference %s was not checked out.' % (refname,) - ) - - # If we reach this point, then the scratch reference exists and is - # checked out. Now check whether there is staged content that - # can be committed: - if self.git.commit_user_merge(edit_log_msg=edit_log_msg): - commit = self.git.get_commit_sha1('HEAD') - - self.git.require_clean_work_tree('proceed') - - merge_frontier = MergeFrontier.map_known_frontier(self) - - # This might throw ManualMergeUnusableError: - (i1, i2) = self.incorporate_manual_merge(commit) - - # Now detach head so that we can delete refname. - self.git.detach('Detach HEAD from %s' % (refname,)) - - self.git.delete_ref( - refname, 'imerge %s: remove scratch reference' % (self.name,), - ) - - try: - # This might throw NotABlockingCommitError: - unblocked_block = merge_frontier.get_affected_blocker_block(i1, i2) - unblocked_block[1, 1].record_blocked(False) - sys.stderr.write( - 'Merge has been recorded for merge %d-%d.\n' - % unblocked_block.get_original_indexes(1, 1) - ) - except NotABlockingCommitError: - raise - finally: - self.save() - - def _set_refname(self, refname, commit, force=False): - try: - ref_oldval = self.git.get_commit_sha1(refname) - except ValueError: - # refname doesn't already exist; simply point it at commit: - self.git.update_ref(refname, commit, 'imerge: recording final merge') - self.git.checkout(refname, quiet=True) - else: - # refname already exists. This has two ramifications: - # 1. HEAD might point at it - # 2. We may only fast-forward it (unless force is set) - head_refname = self.git.get_head_refname() - - if not force and not self.git.is_ancestor(ref_oldval, commit): - raise Failure( - '%s cannot be fast-forwarded to %s!' % (refname, commit) - ) - - if head_refname == refname: - self.git.reset_hard(commit) - else: - self.git.update_ref( - refname, commit, 'imerge: recording final merge', - ) - self.git.checkout(refname, quiet=True) - - def simplify_to_full(self, refname, force=False): - for i1 in range(1, self.len1): - for i2 in range(1, self.len2): - if not (i1, i2) in self: - raise Failure( - 'Cannot simplify to "full" because ' - 'merge %d-%d is not yet done' - % (i1, i2) - ) - - self._set_refname(refname, self[-1, -1].sha1, force=force) - - def simplify_to_rebase_with_history(self, refname, force=False): - i1 = self.len1 - 1 - for i2 in range(1, self.len2): - if not (i1, i2) in self: - raise Failure( - 'Cannot simplify to rebase-with-history because ' - 'merge %d-%d is not yet done' - % (i1, i2) - ) - - commit = self[i1, 0].sha1 - for i2 in range(1, self.len2): - orig = self[0, i2].sha1 - tree = self.git.get_tree(self[i1, i2].sha1) - - # Create a commit, copying the old log message: - msg = ( - self.git.get_log_message(orig).rstrip('\n') - + '\n\n(rebased-with-history from commit %s)\n' % orig - ) - commit = self.git.commit_tree(tree, [commit, orig], msg=msg) - - self._set_refname(refname, commit, force=force) - - def simplify_to_border( - self, refname, - with_history1=False, with_history2=False, force=False, - ): - i1 = self.len1 - 1 - for i2 in range(1, self.len2): - if not (i1, i2) in self: - raise Failure( - 'Cannot simplify to border because ' - 'merge %d-%d is not yet done' - % (i1, i2) - ) - - i2 = self.len2 - 1 - for i1 in range(1, self.len1): - if not (i1, i2) in self: - raise Failure( - 'Cannot simplify to border because ' - 'merge %d-%d is not yet done' - % (i1, i2) - ) - - i1 = self.len1 - 1 - commit = self[i1, 0].sha1 - for i2 in range(1, self.len2 - 1): - orig = self[0, i2].sha1 - tree = self.git.get_tree(self[i1, i2].sha1) - - # Create a commit, copying the old log message: - if with_history2: - parents = [commit, orig] - msg = ( - self.git.get_log_message(orig).rstrip('\n') - + '\n\n(rebased-with-history from commit %s)\n' % (orig,) - ) - else: - parents = [commit] - msg = ( - self.git.get_log_message(orig).rstrip('\n') - + '\n\n(rebased from commit %s)\n' % (orig,) - ) - - commit = self.git.commit_tree(tree, parents, msg=msg) - commit1 = commit - - i2 = self.len2 - 1 - commit = self[0, i2].sha1 - for i1 in range(1, self.len1 - 1): - orig = self[i1, 0].sha1 - tree = self.git.get_tree(self[i1, i2].sha1) - - # Create a commit, copying the old log message: - if with_history1: - parents = [orig, commit] - msg = ( - self.git.get_log_message(orig).rstrip('\n') - + '\n\n(rebased-with-history from commit %s)\n' % (orig,) - ) - else: - parents = [commit] - msg = ( - self.git.get_log_message(orig).rstrip('\n') - + '\n\n(rebased from commit %s)\n' % (orig,) - ) - - commit = self.git.commit_tree(tree, parents, msg=msg) - commit2 = commit - - # Construct the apex commit: - tree = self.git.get_tree(self[-1, -1].sha1) - msg = ( - 'Merge %s into %s (using imerge border)' - % (self.tip2, self.tip1) - ) - - commit = self.git.commit_tree(tree, [commit1, commit2], msg=msg) - - # Update the reference: - self._set_refname(refname, commit, force=force) - - def _simplify_to_path(self, refname, base, path, force=False): - """Simplify based on path and set refname to the result. - - The base and path arguments are defined similarly to - create_commit_chain(), except that instead of SHA-1s they may - optionally represent commits via (i1, i2) tuples. - - """ - - def to_sha1(arg): - if type(arg) is tuple: - commit_record = self[arg] - if not commit_record.is_known(): - raise MissingMergeFailure(*arg) - return commit_record.sha1 - else: - return arg - - base_sha1 = to_sha1(base) - path_sha1 = [] - for (commit, metadata) in path: - commit_sha1 = to_sha1(commit) - metadata_sha1 = to_sha1(metadata) - path_sha1.append((commit_sha1, metadata_sha1)) - - # A path simplification is allowed to discard history, as long - # as the *pre-simplification* apex commit is a descendant of - # the branch to be moved. - if path: - apex = path_sha1[-1][0] - else: - apex = base_sha1 - - if not force and not self.git.is_ff(refname, apex): - raise Failure( - '%s cannot be updated to %s without discarding history.\n' - 'Use --force if you are sure, or choose a different reference' - % (refname, apex,) - ) - - # The update is OK, so here we can set force=True: - self._set_refname( - refname, - self.git.create_commit_chain(base_sha1, path_sha1), - force=True, - ) - - def simplify_to_rebase(self, refname, force=False): - i1 = self.len1 - 1 - path = [ - ((i1, i2), (0, i2)) - for i2 in range(1, self.len2) - ] - - try: - self._simplify_to_path(refname, (i1, 0), path, force=force) - except MissingMergeFailure as e: - raise Failure( - 'Cannot simplify to %s because merge %d-%d is not yet done' - % (self.goal, e.i1, e.i2) - ) - - def simplify_to_drop(self, refname, force=False): - try: - base = self.goalopts['base'] - except KeyError: - raise Failure('Goal "drop" was not initialized correctly') - - i2 = self.len2 - 1 - path = [ - ((i1, i2), (i1, 0)) - for i1 in range(1, self.len1) - ] - - try: - self._simplify_to_path(refname, base, path, force=force) - except MissingMergeFailure as e: - raise Failure( - 'Cannot simplify to rebase because merge %d-%d is not yet done' - % (e.i1, e.i2) - ) - - def simplify_to_revert(self, refname, force=False): - self.simplify_to_rebase(refname, force=force) - - def simplify_to_merge(self, refname, force=False): - if not (-1, -1) in self: - raise Failure( - 'Cannot simplify to merge because merge %d-%d is not yet done' - % (self.len1 - 1, self.len2 - 1) - ) - tree = self.git.get_tree(self[-1, -1].sha1) - parents = [self[-1, 0].sha1, self[0, -1].sha1] - - # Create a preliminary commit with a generic commit message: - sha1 = self.git.commit_tree( - tree, parents, - msg='Merge %s into %s (using imerge)' % (self.tip2, self.tip1), - ) - - self._set_refname(refname, sha1, force=force) - - # Now let the user edit the commit log message: - self.git.amend() - - def simplify(self, refname, force=False): - """Simplify this MergeState and save the result to refname. - - The merge must be complete before calling this method.""" - - if self.goal == 'full': - self.simplify_to_full(refname, force=force) - elif self.goal == 'rebase': - self.simplify_to_rebase(refname, force=force) - elif self.goal == 'rebase-with-history': - self.simplify_to_rebase_with_history(refname, force=force) - elif self.goal == 'border': - self.simplify_to_border(refname, force=force) - elif self.goal == 'border-with-history': - self.simplify_to_border(refname, with_history2=True, force=force) - elif self.goal == 'border-with-history2': - self.simplify_to_border( - refname, with_history1=True, with_history2=True, force=force, - ) - elif self.goal == 'drop': - self.simplify_to_drop(refname, force=force) - elif self.goal == 'revert': - self.simplify_to_revert(refname, force=force) - elif self.goal == 'merge': - self.simplify_to_merge(refname, force=force) - else: - raise ValueError('Invalid value for goal (%r)' % (self.goal,)) - - def save(self): - """Write the current MergeState to the repository.""" - - blockers = [] - for i2 in range(0, self.len2): - for i1 in range(0, self.len1): - record = self[i1, i2] - if record.is_known(): - record.save(self.git, self.name, i1, i2) - if record.is_blocked(): - blockers.append((i1, i2)) - - state = dict( - version='.'.join(str(i) for i in STATE_VERSION), - blockers=blockers, - tip1=self.tip1, tip2=self.tip2, - goal=self.goal, - goalopts=self.goalopts, - manual=self.manual, - branch=self.branch, - ) - self.git.write_imerge_state_dict(self.name, state) - - def __str__(self): - return 'MergeState(\'%s\', tip1=\'%s\', tip2=\'%s\', goal=\'%s\')' % ( - self.name, self.tip1, self.tip2, self.goal, - ) - - -def choose_merge_name(git, name): - names = list(git.iter_existing_imerge_names()) - - # If a name was specified, try to use it and fail if not possible: - if name is not None: - if name not in names: - raise Failure('There is no incremental merge called \'%s\'!' % (name,)) - if len(names) > 1: - # Record this as the new default: - git.set_default_imerge_name(name) - return name - - # A name was not specified. Try to use the default name: - default_name = git.get_default_imerge_name() - if default_name: - if git.check_imerge_exists(default_name): - return default_name - else: - # There's no reason to keep the invalid default around: - git.set_default_imerge_name(None) - raise Failure( - 'Warning: The default incremental merge \'%s\' has disappeared.\n' - '(The setting imerge.default has been cleared.)\n' - 'Please try again.' - % (default_name,) - ) - - # If there is exactly one imerge, set it to be the default and use it. - if len(names) == 1 and git.check_imerge_exists(names[0]): - return names[0] - - raise Failure('Please select an incremental merge using --name') - - -def read_merge_state(git, name=None): - return MergeState.read(git, choose_merge_name(git, name)) - - -def cmd_list(parser, options): - git = GitRepository() - names = list(git.iter_existing_imerge_names()) - default_merge = git.get_default_imerge_name() - if not default_merge and len(names) == 1: - default_merge = names[0] - for name in names: - if name == default_merge: - sys.stdout.write('* %s\n' % (name,)) - else: - sys.stdout.write(' %s\n' % (name,)) - - -def cmd_init(parser, options): - git = GitRepository() - git.require_clean_work_tree('proceed') - - if not options.name: - parser.error( - 'Please specify the --name to be used for this incremental merge' - ) - tip1 = git.get_head_refname(short=True) or 'HEAD' - tip2 = options.tip2 - try: - (merge_base, commits1, commits2) = git.get_boundaries( - tip1, tip2, options.first_parent, - ) - except NonlinearAncestryError as e: - if options.first_parent: - parser.error(str(e)) - else: - parser.error('%s\nPerhaps use "--first-parent"?' % (e,)) - - merge_state = MergeState.initialize( - git, options.name, merge_base, - tip1, commits1, - tip2, commits2, - goal=options.goal, manual=options.manual, - branch=(options.branch or options.name), - ) - merge_state.save() - if len(list(git.iter_existing_imerge_names())) > 1: - git.set_default_imerge_name(options.name) - - -def cmd_start(parser, options): - git = GitRepository() - git.require_clean_work_tree('proceed') - - if not options.name: - parser.error( - 'Please specify the --name to be used for this incremental merge' - ) - tip1 = git.get_head_refname(short=True) or 'HEAD' - tip2 = options.tip2 - - try: - (merge_base, commits1, commits2) = git.get_boundaries( - tip1, tip2, options.first_parent, - ) - except NonlinearAncestryError as e: - if options.first_parent: - parser.error(str(e)) - else: - parser.error('%s\nPerhaps use "--first-parent"?' % (e,)) - - merge_state = MergeState.initialize( - git, options.name, merge_base, - tip1, commits1, - tip2, commits2, - goal=options.goal, manual=options.manual, - branch=(options.branch or options.name), - ) - merge_state.save() - if len(list(git.iter_existing_imerge_names())) > 1: - git.set_default_imerge_name(options.name) - - try: - merge_state.auto_complete_frontier() - except FrontierBlockedError as e: - merge_state.request_user_merge(e.i1, e.i2) - else: - sys.stderr.write('Merge is complete!\n') - - -def cmd_merge(parser, options): - git = GitRepository() - git.require_clean_work_tree('proceed') - - tip2 = options.tip2 - - if options.name: - name = options.name - else: - # By default, name the imerge after the branch being merged: - name = tip2 - git.check_imerge_name_format(name) - - tip1 = git.get_head_refname(short=True) - if tip1: - if not options.branch: - # See if we can store the result to the checked-out branch: - try: - git.check_branch_name_format(tip1) - except InvalidBranchNameError: - pass - else: - options.branch = tip1 - else: - tip1 = 'HEAD' - - if not options.branch: - if options.name: - options.branch = options.name - else: - parser.error( - 'HEAD is not a simple branch. ' - 'Please specify --branch for storing results.' - ) - - try: - (merge_base, commits1, commits2) = git.get_boundaries( - tip1, tip2, options.first_parent, - ) - except NonlinearAncestryError as e: - if options.first_parent: - parser.error(str(e)) - else: - parser.error('%s\nPerhaps use "--first-parent"?' % (e,)) - except NothingToDoError as e: - sys.stdout.write('Already up-to-date.\n') - sys.exit(0) - - merge_state = MergeState.initialize( - git, name, merge_base, - tip1, commits1, - tip2, commits2, - goal=options.goal, manual=options.manual, - branch=options.branch, - ) - merge_state.save() - if len(list(git.iter_existing_imerge_names())) > 1: - git.set_default_imerge_name(name) - - try: - merge_state.auto_complete_frontier() - except FrontierBlockedError as e: - merge_state.request_user_merge(e.i1, e.i2) - else: - sys.stderr.write('Merge is complete!\n') - - -def cmd_rebase(parser, options): - git = GitRepository() - git.require_clean_work_tree('proceed') - - tip1 = options.tip1 - - tip2 = git.get_head_refname(short=True) - if tip2: - if not options.branch: - # See if we can store the result to the current branch: - try: - git.check_branch_name_format(tip2) - except InvalidBranchNameError: - pass - else: - options.branch = tip2 - if not options.name: - # By default, name the imerge after the branch being rebased: - options.name = tip2 - else: - tip2 = git.rev_parse('HEAD') - - if not options.name: - parser.error( - 'The checked-out branch could not be used as the imerge name.\n' - 'Please use the --name option.' - ) - - if not options.branch: - if options.name: - options.branch = options.name - else: - parser.error( - 'HEAD is not a simple branch. ' - 'Please specify --branch for storing results.' - ) - - try: - (merge_base, commits1, commits2) = git.get_boundaries( - tip1, tip2, options.first_parent, - ) - except NonlinearAncestryError as e: - if options.first_parent: - parser.error(str(e)) - else: - parser.error('%s\nPerhaps use "--first-parent"?' % (e,)) - except NothingToDoError as e: - sys.stdout.write('Already up-to-date.\n') - sys.exit(0) - - merge_state = MergeState.initialize( - git, options.name, merge_base, - tip1, commits1, - tip2, commits2, - goal=options.goal, manual=options.manual, - branch=options.branch, - ) - merge_state.save() - if len(list(git.iter_existing_imerge_names())) > 1: - git.set_default_imerge_name(options.name) - - try: - merge_state.auto_complete_frontier() - except FrontierBlockedError as e: - merge_state.request_user_merge(e.i1, e.i2) - else: - sys.stderr.write('Merge is complete!\n') - - -def cmd_drop(parser, options): - git = GitRepository() - git.require_clean_work_tree('proceed') - - m = re.match(r'^(?P.*[^\.])(?P\.{2,})(?P[^\.].*)$', options.range) - if m: - if m.group('sep') != '..': - parser.error( - 'Range must either be a single commit ' - 'or in the form "commit..commit"' - ) - start = git.rev_parse(m.group('start')) - end = git.rev_parse(m.group('end')) - else: - end = git.rev_parse(options.range) - start = git.rev_parse('%s^' % (end,)) - - try: - to_drop = git.linear_ancestry(start, end, options.first_parent) - except NonlinearAncestryError as e: - if options.first_parent: - parser.error(str(e)) - else: - parser.error('%s\nPerhaps use "--first-parent"?' % (e,)) - - # Suppose we want to drop commits 2 and 3 in the branch below. - # Then we set up an imerge as follows: - # - # o - 0 - 1 - 2 - 3 - 4 - 5 - 6 ← tip1 - # | - # 3⁻¹ - # | - # 2⁻¹ - # - # ↑ - # tip2 - # - # We first use imerge to rebase tip1 onto tip2, then we simplify - # by discarding the sequence (2, 3, 3⁻¹, 2⁻¹) (which together are - # a NOOP). In this case, goalopts would have the following - # contents: - # - # goalopts['base'] = rev_parse(commit1) - - tip1 = git.get_head_refname(short=True) - if tip1: - if not options.branch: - # See if we can store the result to the current branch: - try: - git.check_branch_name_format(tip1) - except InvalidBranchNameError: - pass - else: - options.branch = tip1 - if not options.name: - # By default, name the imerge after the branch being rebased: - options.name = tip1 - else: - tip1 = git.rev_parse('HEAD') - - if not options.name: - parser.error( - 'The checked-out branch could not be used as the imerge name.\n' - 'Please use the --name option.' - ) - - if not options.branch: - if options.name: - options.branch = options.name - else: - parser.error( - 'HEAD is not a simple branch. ' - 'Please specify --branch for storing results.' - ) - - # Create a branch based on end that contains the inverse of the - # commits that we want to drop. This will be tip2: - - git.checkout(end) - for commit in reversed(to_drop): - git.revert(commit) - - tip2 = git.rev_parse('HEAD') - - try: - (merge_base, commits1, commits2) = git.get_boundaries( - tip1, tip2, options.first_parent, - ) - except NonlinearAncestryError as e: - if options.first_parent: - parser.error(str(e)) - else: - parser.error('%s\nPerhaps use "--first-parent"?' % (e,)) - except NothingToDoError as e: - sys.stdout.write('Already up-to-date.\n') - sys.exit(0) - - merge_state = MergeState.initialize( - git, options.name, merge_base, - tip1, commits1, - tip2, commits2, - goal='drop', goalopts={'base' : start}, - manual=options.manual, - branch=options.branch, - ) - merge_state.save() - if len(list(git.iter_existing_imerge_names())) > 1: - git.set_default_imerge_name(options.name) - - try: - merge_state.auto_complete_frontier() - except FrontierBlockedError as e: - merge_state.request_user_merge(e.i1, e.i2) - else: - sys.stderr.write('Merge is complete!\n') - - -def cmd_revert(parser, options): - git = GitRepository() - git.require_clean_work_tree('proceed') - - m = re.match(r'^(?P.*[^\.])(?P\.{2,})(?P[^\.].*)$', options.range) - if m: - if m.group('sep') != '..': - parser.error( - 'Range must either be a single commit ' - 'or in the form "commit..commit"' - ) - start = git.rev_parse(m.group('start')) - end = git.rev_parse(m.group('end')) - else: - end = git.rev_parse(options.range) - start = git.rev_parse('%s^' % (end,)) - - try: - to_revert = git.linear_ancestry(start, end, options.first_parent) - except NonlinearAncestryError as e: - if options.first_parent: - parser.error(str(e)) - else: - parser.error('%s\nPerhaps use "--first-parent"?' % (e,)) - - # Suppose we want to revert commits 2 and 3 in the branch below. - # Then we set up an imerge as follows: - # - # o - 0 - 1 - 2 - 3 - 4 - 5 - 6 ← tip1 - # | - # 3⁻¹ - # | - # 2⁻¹ - # - # ↑ - # tip2 - # - # Then we use imerge to rebase tip2 onto tip1. - - tip1 = git.get_head_refname(short=True) - if tip1: - if not options.branch: - # See if we can store the result to the current branch: - try: - git.check_branch_name_format(tip1) - except InvalidBranchNameError: - pass - else: - options.branch = tip1 - if not options.name: - # By default, name the imerge after the branch being rebased: - options.name = tip1 - else: - tip1 = git.rev_parse('HEAD') - - if not options.name: - parser.error( - 'The checked-out branch could not be used as the imerge name.\n' - 'Please use the --name option.' - ) - - if not options.branch: - if options.name: - options.branch = options.name - else: - parser.error( - 'HEAD is not a simple branch. ' - 'Please specify --branch for storing results.' - ) - - # Create a branch based on end that contains the inverse of the - # commits that we want to drop. This will be tip2: - - git.checkout(end) - for commit in reversed(to_revert): - git.revert(commit) - - tip2 = git.rev_parse('HEAD') - - try: - (merge_base, commits1, commits2) = git.get_boundaries( - tip1, tip2, options.first_parent, - ) - except NonlinearAncestryError as e: - if options.first_parent: - parser.error(str(e)) - else: - parser.error('%s\nPerhaps use "--first-parent"?' % (e,)) - except NothingToDoError as e: - sys.stdout.write('Already up-to-date.\n') - sys.exit(0) - - merge_state = MergeState.initialize( - git, options.name, merge_base, - tip1, commits1, - tip2, commits2, - goal='revert', - manual=options.manual, - branch=options.branch, - ) - merge_state.save() - if len(list(git.iter_existing_imerge_names())) > 1: - git.set_default_imerge_name(options.name) - - try: - merge_state.auto_complete_frontier() - except FrontierBlockedError as e: - merge_state.request_user_merge(e.i1, e.i2) - else: - sys.stderr.write('Merge is complete!\n') - - -def cmd_remove(parser, options): - git = GitRepository() - MergeState.remove(git, choose_merge_name(git, options.name)) - - -def cmd_continue(parser, options): - git = GitRepository() - merge_state = read_merge_state(git, options.name) - try: - merge_state.incorporate_user_merge(edit_log_msg=options.edit) - except NoManualMergeError: - pass - except NotABlockingCommitError as e: - raise Failure(str(e)) - except ManualMergeUnusableError as e: - raise Failure(str(e)) - - try: - merge_state.auto_complete_frontier() - except FrontierBlockedError as e: - merge_state.request_user_merge(e.i1, e.i2) - else: - sys.stderr.write('Merge is complete!\n') - - -def cmd_record(parser, options): - git = GitRepository() - merge_state = read_merge_state(git, options.name) - try: - merge_state.incorporate_user_merge(edit_log_msg=options.edit) - except NoManualMergeError as e: - raise Failure(str(e)) - except NotABlockingCommitError: - raise Failure(str(e)) - except ManualMergeUnusableError as e: - raise Failure(str(e)) - - try: - merge_state.auto_complete_frontier() - except FrontierBlockedError as e: - pass - else: - sys.stderr.write('Merge is complete!\n') - - -def cmd_autofill(parser, options): - git = GitRepository() - git.require_clean_work_tree('proceed') - merge_state = read_merge_state(git, options.name) - with git.temporary_head(message='imerge: restoring'): - try: - merge_state.auto_complete_frontier() - except FrontierBlockedError as e: - raise Failure(str(e)) - - -def cmd_simplify(parser, options): - git = GitRepository() - git.require_clean_work_tree('proceed') - merge_state = read_merge_state(git, options.name) - merge_frontier = MergeFrontier.map_known_frontier(merge_state) - if not merge_frontier.is_complete(): - raise Failure('Merge %s is not yet complete!' % (merge_state.name,)) - refname = 'refs/heads/%s' % ((options.branch or merge_state.branch),) - if options.goal is not None: - merge_state.set_goal(options.goal) - merge_state.save() - merge_state.simplify(refname, force=options.force) - - -def cmd_finish(parser, options): - git = GitRepository() - git.require_clean_work_tree('proceed') - merge_state = read_merge_state(git, options.name) - merge_frontier = MergeFrontier.map_known_frontier(merge_state) - if not merge_frontier.is_complete(): - raise Failure('Merge %s is not yet complete!' % (merge_state.name,)) - refname = 'refs/heads/%s' % ((options.branch or merge_state.branch),) - if options.goal is not None: - merge_state.set_goal(options.goal) - merge_state.save() - merge_state.simplify(refname, force=options.force) - MergeState.remove(git, merge_state.name) - - -def cmd_diagram(parser, options): - git = GitRepository() - if not (options.commits or options.frontier): - options.frontier = True - if not (options.color or (options.color is None and sys.stdout.isatty())): - AnsiColor.disable() - - merge_state = read_merge_state(git, options.name) - if options.commits: - merge_state.write(sys.stdout) - sys.stdout.write('\n') - if options.frontier: - merge_frontier = MergeFrontier.map_known_frontier(merge_state) - merge_frontier.write(sys.stdout) - sys.stdout.write('\n') - if options.html: - merge_frontier = MergeFrontier.map_known_frontier(merge_state) - html = open(options.html, 'w') - merge_frontier.write_html(html, merge_state.name) - html.close() - sys.stdout.write( - 'Key:\n' - ) - if options.frontier: - sys.stdout.write( - ' |,-,+ = rectangles forming current merge frontier\n' - ) - sys.stdout.write( - ' * = merge done manually\n' - ' . = merge done automatically\n' - ' # = conflict that is currently blocking progress\n' - ' @ = merge was blocked but has been resolved\n' - ' ? = no merge recorded\n' - '\n' - ) - - -def reparent_recursively(git, start_commit, parents, end_commit): - """Change the parents of start_commit and its descendants. - - Change start_commit to have the specified parents, and reparent - all commits on the ancestry path between start_commit and - end_commit accordingly. Return the replacement end_commit. - start_commit, parents, and end_commit must all be resolved OIDs. - - """ - - # A map {old_oid : new_oid} keeping track of which replacements - # have to be made: - replacements = {} - - # Reparent start_commit: - replacements[start_commit] = git.reparent(start_commit, parents) - - for (commit, parents) in git.rev_list_with_parents( - '--ancestry-path', '--topo-order', '--reverse', - '%s..%s' % (start_commit, end_commit) - ): - parents = [replacements.get(p, p) for p in parents] - replacements[commit] = git.reparent(commit, parents) - - try: - return replacements[end_commit] - except KeyError: - raise ValueError( - "%s is not an ancestor of %s" % (start_commit, end_commit), - ) - - -def cmd_reparent(parser, options): - git = GitRepository() - try: - commit = git.get_commit_sha1(options.commit) - except ValueError: - sys.exit('%s is not a valid commit', options.commit) - - try: - head = git.get_commit_sha1('HEAD') - except ValueError: - sys.exit('HEAD is not a valid commit') - - try: - parents = [git.get_commit_sha1(p) for p in options.parents] - except ValueError as e: - sys.exit(e.message) - - sys.stderr.write('Reparenting %s..HEAD\n' % (options.commit,)) - - try: - new_head = reparent_recursively(git, commit, parents, head) - except ValueError as e: - sys.exit(e.message) - - sys.stdout.write('%s\n' % (new_head,)) - - -def main(args): - NAME_INIT_HELP = 'name to use for this incremental merge' - - def add_name_argument(subparser, help=None): - if help is None: - subcommand = subparser.prog.split()[1] - help = 'name of incremental merge to {0}'.format(subcommand) - - subparser.add_argument( - '--name', action='store', default=None, help=help, - ) - - def add_goal_argument(subparser, default=DEFAULT_GOAL): - help = 'the goal of the incremental merge' - if default is None: - help = ( - 'the type of simplification to be made ' - '(default is the value provided to "init" or "start")' - ) - subparser.add_argument( - '--goal', - action='store', default=default, - choices=ALLOWED_GOALS, - help=help, - ) - - def add_branch_argument(subparser): - subcommand = subparser.prog.split()[1] - help = 'the name of the branch to which the result will be stored' - if subcommand in ['simplify', 'finish']: - help = ( - 'the name of the branch to which to store the result ' - '(default is the value provided to "init" or "start" if any; ' - 'otherwise the name of the merge). ' - 'If BRANCH already exists then it must be able to be ' - 'fast-forwarded to the result unless the --force option is ' - 'specified.' - ) - subparser.add_argument( - '--branch', - action='store', default=None, - help=help, - ) - - def add_manual_argument(subparser): - subparser.add_argument( - '--manual', - action='store_true', default=False, - help=( - 'ask the user to complete all merges manually, even when they ' - 'appear conflict-free. This option disables the usual bisection ' - 'algorithm and causes the full incremental merge diagram to be ' - 'completed.' - ), - ) - - def add_first_parent_argument(subparser, default=None): - subcommand = subparser.prog.split()[1] - help = ( - 'handle only the first parent commits ' - '(this option is currently required if the history is nonlinear)' - ) - if subcommand in ['merge', 'rebase']: - help = argparse.SUPPRESS - subparser.add_argument( - '--first-parent', action='store_true', default=default, help=help, - ) - - def add_tip2_argument(subparser): - subparser.add_argument( - 'tip2', action='store', metavar='branch', - help='the tip of the branch to be merged into HEAD', - ) - - parser = argparse.ArgumentParser( - description=__doc__, - formatter_class=argparse.RawDescriptionHelpFormatter, - ) - subparsers = parser.add_subparsers(dest='subcommand', help='sub-command') - - subparser = subparsers.add_parser( - 'start', - help=( - 'start a new incremental merge ' - '(equivalent to "init" followed by "continue")' - ), - ) - add_name_argument(subparser, help=NAME_INIT_HELP) - add_goal_argument(subparser) - add_branch_argument(subparser) - add_manual_argument(subparser) - add_first_parent_argument(subparser) - add_tip2_argument(subparser) - - subparser = subparsers.add_parser( - 'merge', - help='start a simple merge via incremental merge', - ) - add_name_argument(subparser, help=NAME_INIT_HELP) - add_goal_argument(subparser, default='merge') - add_branch_argument(subparser) - add_manual_argument(subparser) - add_first_parent_argument(subparser, default=True) - add_tip2_argument(subparser) - - subparser = subparsers.add_parser( - 'rebase', - help='start a simple rebase via incremental merge', - ) - add_name_argument(subparser, help=NAME_INIT_HELP) - add_goal_argument(subparser, default='rebase') - add_branch_argument(subparser) - add_manual_argument(subparser) - add_first_parent_argument(subparser, default=True) - subparser.add_argument( - 'tip1', action='store', metavar='branch', - help=( - 'the tip of the branch onto which the current branch should ' - 'be rebased' - ), - ) - - subparser = subparsers.add_parser( - 'drop', - help='drop one or more commits via incremental merge', - ) - add_name_argument(subparser, help=NAME_INIT_HELP) - add_branch_argument(subparser) - add_manual_argument(subparser) - add_first_parent_argument(subparser, default=True) - subparser.add_argument( - 'range', action='store', metavar='[commit | commit..commit]', - help=( - 'the commit or range of commits that should be dropped' - ), - ) - - subparser = subparsers.add_parser( - 'revert', - help='revert one or more commits via incremental merge', - ) - add_name_argument(subparser, help=NAME_INIT_HELP) - add_branch_argument(subparser) - add_manual_argument(subparser) - add_first_parent_argument(subparser, default=True) - subparser.add_argument( - 'range', action='store', metavar='[commit | commit..commit]', - help=( - 'the commit or range of commits that should be reverted' - ), - ) - - subparser = subparsers.add_parser( - 'continue', - help=( - 'record the merge at branch imerge/NAME ' - 'and start the next step of the merge ' - '(equivalent to "record" followed by "autofill" ' - 'and then sets up the working copy with the next ' - 'conflict that has to be resolved manually)' - ), - ) - add_name_argument(subparser) - subparser.set_defaults(edit=None) - subparser.add_argument( - '--edit', '-e', dest='edit', action='store_true', - help='commit staged changes with the --edit option', - ) - subparser.add_argument( - '--no-edit', dest='edit', action='store_false', - help='commit staged changes with the --no-edit option', - ) - - subparser = subparsers.add_parser( - 'finish', - help=( - 'simplify then remove a completed incremental merge ' - '(equivalent to "simplify" followed by "remove")' - ), - ) - add_name_argument(subparser) - add_goal_argument(subparser, default=None) - add_branch_argument(subparser) - subparser.add_argument( - '--force', - action='store_true', default=False, - help='allow the target branch to be updated in a non-fast-forward manner', - ) - - subparser = subparsers.add_parser( - 'diagram', - help='display a diagram of the current state of a merge', - ) - add_name_argument(subparser) - subparser.add_argument( - '--commits', action='store_true', default=False, - help='show the merges that have been made so far', - ) - subparser.add_argument( - '--frontier', action='store_true', default=False, - help='show the current merge frontier', - ) - subparser.add_argument( - '--html', action='store', default=None, - help='generate HTML diagram showing the current merge frontier', - ) - subparser.add_argument( - '--color', dest='color', action='store_true', default=None, - help='draw diagram with colors', - ) - subparser.add_argument( - '--no-color', dest='color', action='store_false', - help='draw diagram without colors', - ) - - subparser = subparsers.add_parser( - 'list', - help=( - 'list the names of incremental merges that are currently in progress. ' - 'The active merge is shown with an asterisk next to it.' - ), - ) - - subparser = subparsers.add_parser( - 'init', - help='initialize a new incremental merge', - ) - add_name_argument(subparser, help=NAME_INIT_HELP) - add_goal_argument(subparser) - add_branch_argument(subparser) - add_manual_argument(subparser) - add_first_parent_argument(subparser) - add_tip2_argument(subparser) - - subparser = subparsers.add_parser( - 'record', - help='record the merge at branch imerge/NAME', - ) - # record: - add_name_argument( - subparser, - help='name of merge to which the merge should be added', - ) - subparser.set_defaults(edit=None) - subparser.add_argument( - '--edit', '-e', dest='edit', action='store_true', - help='commit staged changes with the --edit option', - ) - subparser.add_argument( - '--no-edit', dest='edit', action='store_false', - help='commit staged changes with the --no-edit option', - ) - - subparser = subparsers.add_parser( - 'autofill', - help='autofill non-conflicting merges', - ) - add_name_argument(subparser) - - subparser = subparsers.add_parser( - 'simplify', - help=( - 'simplify a completed incremental merge by discarding unneeded ' - 'intermediate merges and cleaning up the ancestry of the commits ' - 'that are retained' - ), - ) - add_name_argument(subparser) - add_goal_argument(subparser, default=None) - add_branch_argument(subparser) - subparser.add_argument( - '--force', - action='store_true', default=False, - help='allow the target branch to be updated in a non-fast-forward manner', - ) - - subparser = subparsers.add_parser( - 'remove', - help='irrevocably remove an incremental merge', - ) - add_name_argument(subparser) - - subparser = subparsers.add_parser( - 'reparent', - help=( - 'change the parents of the specified commit and propagate the ' - 'change to HEAD' - ), - ) - subparser.add_argument( - '--commit', metavar='COMMIT', default='HEAD', - help=( - 'target commit to reparent. Create a new commit identical to ' - 'this one, but having the specified parents. Then create ' - 'new versions of all descendants of this commit all the way to ' - 'HEAD, incorporating the modified commit. Output the SHA-1 of ' - 'the replacement HEAD commit.' - ), - ) - subparser.add_argument( - 'parents', nargs='*', metavar='PARENT', - help='a list of commits', - ) - - options = parser.parse_args(args) - - # Set an environment variable GIT_IMERGE=1 while we are running. - # This makes it possible for hook scripts etc. to know that they - # are being run within git-imerge, and should perhaps behave - # differently. In the future we might make the value more - # informative, like GIT_IMERGE=[automerge|autofill|...]. - os.environ[str('GIT_IMERGE')] = str('1') - - if options.subcommand == 'list': - cmd_list(parser, options) - elif options.subcommand == 'init': - cmd_init(parser, options) - elif options.subcommand == 'start': - cmd_start(parser, options) - elif options.subcommand == 'merge': - cmd_merge(parser, options) - elif options.subcommand == 'rebase': - cmd_rebase(parser, options) - elif options.subcommand == 'drop': - cmd_drop(parser, options) - elif options.subcommand == 'revert': - cmd_revert(parser, options) - elif options.subcommand == 'remove': - cmd_remove(parser, options) - elif options.subcommand == 'continue': - cmd_continue(parser, options) - elif options.subcommand == 'record': - cmd_record(parser, options) - elif options.subcommand == 'autofill': - cmd_autofill(parser, options) - elif options.subcommand == 'simplify': - cmd_simplify(parser, options) - elif options.subcommand == 'finish': - cmd_finish(parser, options) - elif options.subcommand == 'diagram': - cmd_diagram(parser, options) - elif options.subcommand == 'reparent': - cmd_reparent(parser, options) - else: - parser.error('Unrecognized subcommand') - - -if __name__ == '__main__': - try: - main(sys.argv[1:]) - except Failure as e: - sys.exit(str(e)) - - -# vim: set expandtab ft=python: diff --git a/local/.bin/hornet/twitch b/local/.bin/hornet/twitch deleted file mode 100755 index 5b691ab..0000000 --- a/local/.bin/hornet/twitch +++ /dev/null @@ -1,9 +0,0 @@ -#!/bin/sh - -if [ -z "${1}" ]; then - ssh partofme killall vlc - echo '*/buffer close twitch.#' >~/.weechat/weechat_fifo -else - ssh partofme sh -c "'nohup twitch ${1} > /dev/null 2>&1 &'" - echo "irc.server.twitch */join #${1}" >~/.weechat/weechat_fifo -fi diff --git a/local/.bin/nopaste b/local/.bin/nopaste deleted file mode 100755 index 573af2e..0000000 --- a/local/.bin/nopaste +++ /dev/null @@ -1,37 +0,0 @@ -#!/usr/bin/env perl -use strict; -use warnings; -use 5.020; - -use Carp; -use File::Spec; -use File::Temp; -use POSIX 'strftime'; - -if (@ARGV > 1) { - croak "can only nopaste one file at a time"; -} - -my $date = strftime("%Y-%m-%d", localtime); -my $template = "${date}-XXXXXXXX"; -my $suffix = @ARGV - ? "-${\(File::Spec->splitdir($ARGV[0]))[-1]}" - : undef; - -my $contents = do { local $/; <> }; - -my $tmpfile = File::Temp->new( - TEMPLATE => $template, - SUFFIX => $suffix, - UNLINK => 1, - TMPDIR => 1, -); -my $tmpfilename = $tmpfile->filename; - -print $tmpfile $contents or croak "Can't write to $tmpfilename: $!"; -close $tmpfile or croak "Can't write to $tmpfilename: $!"; -chmod 0644 => $tmpfilename; - -system('scp', '-pq', $tmpfilename, "tozt.net:paste"); - -say "https://paste.tozt.net/${\(File::Spec->splitdir($tmpfilename))[-1]}"; diff --git a/local/.bin/pick-music b/local/.bin/pick-music deleted file mode 100755 index 8c406f7..0000000 --- a/local/.bin/pick-music +++ /dev/null @@ -1,6 +0,0 @@ -#!/bin/sh -set -eu -set -o pipefail - -lastfm-query sync doyster -lastfm-query recommend "${1:-20}" diff --git a/local/.bin/rand-music b/local/.bin/rand-music deleted file mode 100755 index 6e9bccc..0000000 --- a/local/.bin/rand-music +++ /dev/null @@ -1,13 +0,0 @@ -#!/bin/sh -set -eu -set -o pipefail - -if [ "${1:-}" = '--old' ]; then - extra_args="--exclude yearly --include all" - shift -else - extra_args= -fi - -lastfm-query sync doyster -lastfm-query recommend --random --album $extra_args "${1:-20}" diff --git a/local/.bin/ringtone b/local/.bin/ringtone deleted file mode 100755 index 19dddb6..0000000 --- a/local/.bin/ringtone +++ /dev/null @@ -1,20 +0,0 @@ -#!/bin/sh -set -eu -set -o pipefail - -if [ $# -lt 3 ]; then - echo "usage: ringtone " - echo "note: fake_video_file is unused, but must be valid" - echo " and at least the length of the output" - exit 1 -fi - -mencoder \ - -ovc frameno \ - -oac mp3lame \ - -lameopts cbr:br=64 \ - -of rawaudio \ - -o "$2" \ - -audiofile "$1" \ - -endpos 256kb \ - "$3" diff --git a/local/.bin/tozt/learn_spam b/local/.bin/tozt/learn_spam deleted file mode 100755 index 75e500a..0000000 --- a/local/.bin/tozt/learn_spam +++ /dev/null @@ -1,38 +0,0 @@ -#!/bin/sh -set -eu -set -o pipefail - -MAILDIR=~/Maildir -OPTS="--no-sync" -SALEARN=/usr/bin/vendor_perl/sa-learn - -if [ "$1" = "-q" ]; then - OPTS="$OPTS" - OUTFILE="/dev/null" -else - OPTS="$OPTS --showdots" - OUTFILE="/dev/stdout" -fi - -if [ ! "$1" = "-q" ]; then - echo "$(date): Learning ham..." -fi -nice find $MAILDIR -type f | - perl -nl0e'chomp; ($ts) = /(\d{10})/; $now = time; print if m{/(new|cur)/} && !m{/(\.old|\.spam)/} && $ts > $now - 60*60*24*30' | - xargs -n1000 -0 $SALEARN "$OPTS" --ham >$OUTFILE - -if [ ! "$1" = "-q" ]; then - echo "$(date): Learning spam..." -fi -nice find $MAILDIR -type f | - perl -nl0e'chomp; ($ts) = /(\d{10})/; $now = time; print if m{/(new|cur)/} && m{/\.spam/} && $ts > $now - 60*60*24*30' | - xargs -n1000 -0 $SALEARN "$OPTS" --spam >$OUTFILE - -if [ ! "$1" = "-q" ]; then - echo "$(date): Syncing..." -fi -nice $SALEARN --sync >$OUTFILE - -if [ ! "$1" = "-q" ]; then - echo "$(date): Done!" -fi diff --git a/local/.bin/update-addressbook b/local/.bin/update-addressbook deleted file mode 100755 index 4683e86..0000000 --- a/local/.bin/update-addressbook +++ /dev/null @@ -1,276 +0,0 @@ -#!/usr/bin/env perl -use strict; -use warnings; -use 5.020; -use feature 'signatures'; -no warnings 'experimental::signatures'; - -use Config::INI::Reader; -use Config::INI::Writer; -use Email::Address; -use File::Find; - -my @exclude_patterns = ( - # quoted-printable (needs special handling, punting for now) - sub($address) { $address->format =~ /^=\?/ }, - # automated emails - sub($address) { $address->address =~ /(mailer-daemon|noreply)/i }, -); - -# abook uses # for comments instead of ; -package Abook::Reader { - use base 'Config::INI::Reader'; - - sub preprocess_line($self, $line) { - ${$line} =~ s/\s+#.*$//g; - } - - sub can_ignore($self, $line, $) { - return $line =~ /\A\s*(?:#|$)/ ? 1 : 0; - } -} - -# abook is super finicky about its input format -package Abook::Writer { - use base 'Config::INI::Writer'; - - sub write_handle($self, $input, $handle) { - print $handle "# abook addressbook file\n\n"; - $self->SUPER::write_handle($input, $handle); - } - - sub preprocess_input($self, $data) { - my $ini_data = [ - format => { - program => 'abook', - version => '0.6.1', - }, - ]; - my $i = 0; - for my $name (sort { fc($a) cmp fc($b) } keys %$data) { - my $person_data = $data->{$name}->as_hashref; - delete $person_data->{name}; - push @$ini_data, ( - $i++ => [ - name => $name, - %$person_data, - ], - ); - } - - $self->SUPER::preprocess_input($ini_data); - } - - sub stringify_value_assignment($self, $name, $value) { - return '' unless defined $value; - return $name . '=' . $self->stringify_value($value) . "\n"; - } -} - -package Person { - sub from_hashref($class, $data) { - return bless $data, $class; - } - - sub from_email($class, $name, $addresses) { - return $class->from_hashref({ - name => $name, - email => join(',', map { $_->address } @$addresses), - }); - } - - sub addresses($self) { - return split(',', $self->{email} // ''); - } - - sub as_hashref($self) { - return { %$self }; - } -} - -sub existing_people($addressbook) { - my $data = Abook::Reader->read_file($addressbook); - delete $data->{format}; - - my %people; - for my $id (keys %$data) { - my $person = $data->{$id}; - $people{$person->{name}} = Person->from_hashref($person); - } - - return %people; -} - -sub maildir_addresses($maildir) { - my %addresses; - - find(sub() { - open my $fh, '<', $_ or die "couldn't open $_: $!"; - while (<$fh>) { - last if /^$/; - next unless /^(?:From|Sender): /; - for my $address (Email::Address->parse($_)) { - $address = Email::Address->new( - $address->name, - $address->address, - ); - - my $name = $address->name; - my $format = $address->format; - - next if $addresses{$name} && - grep { $format eq $_ } @{ $addresses{$name} }; - next if grep { $_->($address) } @exclude_patterns; - - push @{ $addresses{$name} ||= [] }, $address; - } - } - close $fh; - }, $maildir); - - return %addresses; -} - -sub merge_addresses($old, $new) { - my %reverse_old = map { - map { $_->address => $_->name } @$_ - } values %$old; - my @new_addresses = map { - map { $_->address } @$_ - } values %$new; - - my %seen_address; - my %reverse_ret; - for my $new_address (@new_addresses) { - next if $seen_address{$new_address}++; - - my @related_addresses = ($new_address); - my @related_names; - while (1) { - my @new_related_names = map { - my $cur_address = $_; - ( - (grep { - grep { - fc($_->address) eq fc($cur_address) - } @{ $new->{$_} } - } keys %$new), - (grep { - grep { - fc($_->address) eq fc($cur_address) - } @{ $old->{$_} } - } keys %$old), - ) - } @related_addresses; - @new_related_names = keys( - %{ { map { $_ => 1 } @new_related_names } } - ); - - my @new_related_addresses = map { - $_->address - } map { - ( - (exists $new->{$_} - ? (@{ $new->{$_} }) - : ()), - (exists $old->{$_} - ? (@{ $old->{$_} }) - : ()), - ) - } @new_related_names; - @new_related_addresses = keys( - %{ { map { $_ => 1 } @new_related_addresses } } - ); - - last if @related_names == @new_related_names - && @related_addresses == @new_related_addresses; - - @related_addresses = @new_related_addresses; - @related_names = @new_related_names; - } - - my ($name) = grep { exists $old->{$_} } @related_names; - $name = (sort @related_names)[0] unless defined $name; - - for my $related_address (@related_addresses) { - $seen_address{$related_address}++; - $reverse_ret{$related_address} = $name; - } - } - - %reverse_ret = (%reverse_old, %reverse_ret); - - my %ret; - for my $address (keys %reverse_ret) { - my $name = $reverse_ret{$address}; - push @{ $ret{$name} ||= [] }, Email::Address->new($name, $address); - } - - for my $name (keys %ret) { - my %seen_name; - for my $address (@{ $ret{$name} }) { - if (defined $seen_name{lc($address->address)}) { - if (defined $reverse_old{$address->address}) { - $seen_name{lc($address->address)} = $address; - } - } - else { - $seen_name{lc($address->address)} = $address; - } - } - $ret{$name} = [ values %seen_name ]; - } - - return %ret; -} - -sub merge_people($old, $new) { - for my $name (keys %$new) { - if (exists $old->{$name}) { - my $old_person = $old->{$name}; - my @addresses = $old_person->addresses; - for my $address ($new->{$name}->addresses) { - push @addresses, $address - unless grep { $_ eq $address } @addresses; - } - - $old_person->{email} = join(',', @addresses); - } - else { - $old->{$name} = $new->{$name}; - } - } - - return %$old; -} - -sub main($addressbook, $maildir) { - die "usage: $0 ADDRESSBOOK MAILDIR" unless @_ == 2; - - my %existing_people = existing_people($addressbook); - my %existing_addresses = map { - my $name = $_; - $name => [ - map { - Email::Address->new($name, $_) - } $existing_people{$name}->addresses - ] - } keys %existing_people; - - my %maildir = maildir_addresses($maildir); - - my %new_addresses = merge_addresses(\%existing_addresses, \%maildir); - my %new_people = map { - $_ => Person->from_email($_, $new_addresses{$_}) - } keys %new_addresses; - - my %new_abook = merge_people(\%existing_people, \%new_people); - - rename $addressbook => "$addressbook.bak" - or die "couldn't rename $addressbook: $!"; - - my $writer = Abook::Writer->new; - $writer->write_file(\%new_abook, $addressbook); -} - -main(@ARGV); diff --git a/mpd/.config/mpd/mpd.conf b/mpd/.config/mpd/mpd.conf deleted file mode 100644 index e30c2fc..0000000 --- a/mpd/.config/mpd/mpd.conf +++ /dev/null @@ -1,5 +0,0 @@ -db_file "~/.cache/mpd/mpd.db" -state_file "~/.cache/mpd/mpd.state" -playlist_directory "~/.config/mpd/playlists" -music_directory "~/media/audio/copy" -bind_to_address "localhost" diff --git a/mpd/.services/available/mpd/log/run b/mpd/.services/available/mpd/log/run deleted file mode 100755 index 072bb30..0000000 --- a/mpd/.services/available/mpd/log/run +++ /dev/null @@ -1,4 +0,0 @@ -#!/bin/sh -set -eu - -exec multilog t s16777215 "$HOME/.log/mpd" diff --git a/mpd/.services/available/mpd/run b/mpd/.services/available/mpd/run deleted file mode 100755 index 186baef..0000000 --- a/mpd/.services/available/mpd/run +++ /dev/null @@ -1,3 +0,0 @@ -#!/bin/sh - -exec mpd --no-daemon 2>&1 diff --git a/mpd/Makefile b/mpd/Makefile deleted file mode 100644 index b251d30..0000000 --- a/mpd/Makefile +++ /dev/null @@ -1,6 +0,0 @@ -include ../Makefile.include - -install: $(HOME)/.services/enabled/mpd - @mkdir -p $(HOME)/.log/mpd - @mkdir -p $(HOME)/.cache/mpd - @mkdir -p $(HOME)/.config/mpd/playlists diff --git a/ncmpcpp/.config/ncmpcpp/bindings b/ncmpcpp/.config/ncmpcpp/bindings deleted file mode 100644 index 20a0259..0000000 --- a/ncmpcpp/.config/ncmpcpp/bindings +++ /dev/null @@ -1,591 +0,0 @@ -############################################################## -## This is the example bindings file. Copy it to ## -## ~/.ncmpcpp/bindings or $XDG_CONFIG_HOME/ncmpcpp/bindings ## -## and set up your preferences ## -############################################################## -## -##### General rules ##### -## -## 1) Because each action has runtime checks whether it's -## ok to run it, a few actions can be bound to one key. -## Actions will be bound in order given in configuration -## file. When a key is pressed, first action in order -## will test itself whether it's possible to run it. If -## test succeeds, action is executed and other actions -## bound to this key are ignored. If it doesn't, next -## action in order tests itself etc. -## -## 2) It's possible to bind more that one action at once -## to a key. It can be done using the following syntax: -## -## def_key "key" -## action1 -## action2 -## ... -## -## This creates a chain of actions. When such chain is -## executed, each action in chain is run until the end of -## chain is reached or one of its actions fails to execute -## due to its requirements not being met. If multiple actions -## and/or chains are bound to the same key, they will be -## consecutively run until one of them gets fully executed. -## -## 3) When ncmpcpp starts, bindings configuration file is -## parsed and then ncmpcpp provides "missing pieces" -## of default keybindings. If you want to disable some -## bindings, there is a special action called 'dummy' -## for that purpose. Eg. if you want to disable ability -## to crop playlists, you need to put the following -## into configuration file: -## -## def_key "C" -## dummy -## -## After that ncmpcpp will not bind any default action -## to this key. -## -## 4) To let you write simple macros, the following special -## actions are provided: -## -## - push_character "character" - pushes given special -## character into input queue, so it will be immediately -## picked by ncmpcpp upon next call to readKey function. -## Accepted values: mouse, up, down, page_up, page_down, -## home, end, space, enter, insert, delete, left, right, -## tab, ctrl-a, ctrl-b, ..., ctrl-z, ctrl-[, ctrl-\\, -## ctrl-], ctrl-^, ctrl-_, f1, f2, ..., f12, backspace. -## In addition, most of these names can be prefixed with -## alt-/ctrl-/shift- to be recognized with the appropriate -## modifier key(s). -## -## - push_characters "string" - pushes given string into -## input queue. -## -## - require_runnable "action" - checks whether given action -## is runnable and fails if it isn't. This is especially -## useful when mixed with previous two functions. Consider -## the following macro definition: -## -## def_key "key" -## push_characters "custom_filter" -## apply_filter -## -## If apply_filter can't be currently run, we end up with -## sequence of characters in input queue which will be -## treated just as we typed them. This may lead to unexpected -## results (in this case 'c' will most likely clear current -## playlist, 'u' will trigger database update, 's' will stop -## playback etc.). To prevent such thing from happening, we -## need to change above definition to this one: -## -## def_key "key" -## require_runnable "apply_filter" -## push_characters "custom_filter" -## apply_filter -## -## Here, first we test whether apply_filter can be actually run -## before we stuff characters into input queue, so if condition -## is not met, whole chain is aborted and we're fine. -## -## - require_screen "screen" - checks whether given screen is -## currently active. accepted values: browser, clock, help, -## media_library, outputs, playlist, playlist_editor, -## search_engine, tag_editor, visualizer, last_fm, lyrics, -## selected_items_adder, server_info, song_info, -## sort_playlist_dialog, tiny_tag_editor. -## -## - run_external_command "command" - runs given command using -## system() function. -## -## 5) In addition to binding to a key, you can also bind actions -## or chains of actions to a command. If it comes to commands, -## syntax is very similar to defining keys. Here goes example -## definition of a command: -## -## def_command "quit" [deferred] -## stop -## quit -## -## If you execute the above command (which can be done by -## invoking action execute_command, typing 'quit' and pressing -## enter), ncmpcpp will stop the player and then quit. Note the -## presence of word 'deferred' enclosed in square brackets. It -## tells ncmpcpp to wait for confirmation (ie. pressing enter) -## after you typed quit. Instead of 'deferred', 'immediate' -## could be used. Then ncmpcpp will not wait for confirmation -## (enter) and will execute the command the moment it sees it. -## -## Note: while command chains are executed, internal environment -## update (which includes current window refresh and mpd status -## update) is not performed for performance reasons. However, it -## may be desirable to do so in some situration. Therefore it's -## possible to invoke by hand by performing 'update enviroment' -## action. -## -## Note: There is a difference between: -## -## def_key "key" -## action1 -## -## def_key "key" -## action2 -## -## and -## -## def_key "key" -## action1 -## action2 -## -## First one binds two single actions to the same key whilst -## second one defines a chain of actions. The behavior of -## these two is different and is described in (1) and (2). -## -## Note: Function def_key accepts non-ascii characters. -## -##### List of unbound actions ##### -## -## The following actions are not bound to any key/command: -## -## - set_volume -## - -def_key "j" - scroll_down - -def_key "k" - scroll_up - -def_key "J" - move_sort_order_down - -def_key "K" - move_sort_order_up - -def_key ">" - next - -def_key "<" - previous - -def_key "up" - volume_up - -def_key "down" - volume_down - -def_key "backspace" - stop - -def_key "space" - pause - -def_key "left" - seek_backward - -def_key "right" - seek_forward - -def_key "p" - previous_found_item - -def_key "n" - next_found_item - -def_key "a" - add_item_to_playlist - -def_key "d" - delete_playlist_items - -def_key "g" - move_home - -def_key "G" - move_end - -#def_key "shift-up" -# select_item -# scroll_up -# -#def_key "down" -# scroll_down -# -#def_key "shift-down" -# select_item -# scroll_down -# -#def_key "[" -# scroll_up_album -# -#def_key "]" -# scroll_down_album -# -#def_key "{" -# scroll_up_artist -# -#def_key "}" -# scroll_down_artist -# -#def_key "page_up" -# page_up -# -#def_key "page_down" -# page_down -# -#def_key "home" -# move_home -# -#def_key "end" -# move_end -# -#def_key "insert" -# select_item -# -#def_key "enter" -# enter_directory -# -#def_key "enter" -# toggle_output -# -#def_key "enter" -# run_action -# -#def_key "enter" -# play_item -# -#def_key "space" -# add_item_to_playlist -# -#def_key "space" -# toggle_lyrics_update_on_song_change -# -#def_key "space" -# toggle_visualization_type -# -#def_key "delete" -# delete_playlist_items -# -#def_key "delete" -# delete_browser_items -# -#def_key "delete" -# delete_stored_playlist -# -#def_key "right" -# next_column -# -#def_key "right" -# slave_screen -# -#def_key "right" -# volume_up -# -#def_key "+" -# volume_up -# -#def_key "left" -# previous_column -# -#def_key "left" -# master_screen -# -#def_key "left" -# volume_down -# -#def_key "-" -# volume_down -# -#def_key ":" -# execute_command -# -#def_key "tab" -# next_screen -# -#def_key "shift-tab" -# previous_screen -# -#def_key "f1" -# show_help -# -#def_key "1" -# show_playlist -# -#def_key "2" -# show_browser -# -#def_key "2" -# change_browse_mode -# -#def_key "3" -# show_search_engine -# -#def_key "3" -# reset_search_engine -# -#def_key "4" -# show_media_library -# -#def_key "4" -# toggle_media_library_columns_mode -# -#def_key "5" -# show_playlist_editor -# -#def_key "6" -# show_tag_editor -# -#def_key "7" -# show_outputs -# -#def_key "8" -# show_visualizer -# -#def_key "=" -# show_clock -# -#def_key "@" -# show_server_info -# -#def_key "s" -# stop -# -#def_key "p" -# pause -# -#def_key ">" -# next -# -#def_key "<" -# previous -# -#def_key "ctrl-h" -# jump_to_parent_directory -# -#def_key "ctrl-h" -# replay_song -# -#def_key "backspace" -# jump_to_parent_directory -# -#def_key "backspace" -# replay_song -# -#def_key "f" -# seek_forward -# -#def_key "b" -# seek_backward -# -#def_key "r" -# toggle_repeat -# -#def_key "z" -# toggle_random -# -#def_key "y" -# save_tag_changes -# -#def_key "y" -# start_searching -# -#def_key "y" -# toggle_single -# -#def_key "R" -# toggle_consume -# -#def_key "Y" -# toggle_replay_gain_mode -# -#def_key "T" -# toggle_add_mode -# -#def_key "|" -# toggle_mouse -# -#def_key "#" -# toggle_bitrate_visibility -# -#def_key "Z" -# shuffle -# -#def_key "x" -# toggle_crossfade -# -#def_key "X" -# set_crossfade -# -#def_key "u" -# update_database -# -#def_key "ctrl-s" -# sort_playlist -# -#def_key "ctrl-s" -# toggle_browser_sort_mode -# -#def_key "ctrl-s" -# toggle_media_library_sort_mode -# -#def_key "ctrl-r" -# reverse_playlist -# -#def_key "ctrl-f" -# apply_filter -# -#def_key "ctrl-_" -# select_found_items -# -#def_key "/" -# find -# -#def_key "/" -# find_item_forward -# -#def_key "?" -# find -# -#def_key "?" -# find_item_backward -# -#def_key "." -# next_found_item -# -#def_key "," -# previous_found_item -# -#def_key "w" -# toggle_find_mode -# -#def_key "e" -# edit_song -# -#def_key "e" -# edit_library_tag -# -#def_key "e" -# edit_library_album -# -#def_key "e" -# edit_directory_name -# -#def_key "e" -# edit_playlist_name -# -#def_key "e" -# edit_lyrics -# -#def_key "i" -# show_song_info -# -#def_key "I" -# show_artist_info -# -#def_key "g" -# jump_to_position_in_song -# -#def_key "l" -# show_lyrics -# -#def_key "ctrl-v" -# select_range -# -#def_key "v" -# reverse_selection -# -#def_key "V" -# remove_selection -# -#def_key "B" -# select_album -# -#def_key "a" -# add_selected_items -# -#def_key "c" -# clear_playlist -# -#def_key "c" -# clear_main_playlist -# -#def_key "C" -# crop_playlist -# -#def_key "C" -# crop_main_playlist -# -#def_key "m" -# move_sort_order_up -# -#def_key "m" -# move_selected_items_up -# -#def_key "n" -# move_sort_order_down -# -#def_key "n" -# move_selected_items_down -# -#def_key "M" -# move_selected_items_to -# -#def_key "A" -# add -# -#def_key "S" -# save_playlist -# -#def_key "o" -# jump_to_playing_song -# -#def_key "G" -# jump_to_browser -# -#def_key "G" -# jump_to_playlist_editor -# -#def_key "~" -# jump_to_media_library -# -#def_key "E" -# jump_to_tag_editor -# -#def_key "U" -# toggle_playing_song_centering -# -#def_key "P" -# toggle_display_mode -# -#def_key "\\" -# toggle_interface -# -#def_key "!" -# toggle_separators_between_albums -# -#def_key "L" -# toggle_lyrics_fetcher -# -#def_key "F" -# fetch_lyrics_in_background -# -#def_key "alt-l" -# toggle_fetching_lyrics_in_background -# -#def_key "ctrl-l" -# toggle_screen_lock -# -#def_key "`" -# toggle_library_tag_type -# -#def_key "`" -# refetch_lyrics -# -#def_key "`" -# add_random_items -# -#def_key "ctrl-p" -# set_selected_items_priority -# -#def_key "q" -# quit -# diff --git a/packages.hornet b/packages.hornet index 77a92be..12bcf19 100644 --- a/packages.hornet +++ b/packages.hornet @@ -1,4 +1,3 @@ -abook alacritty bash compton @@ -14,16 +13,13 @@ less local mbsync mbsyncloop -mpd msmtp mutt -ncmpcpp nethack notmuch readline redshift reply -screen sh ssh starship diff --git a/packages.mail b/packages.mail index e4cae92..9edbc1a 100644 --- a/packages.mail +++ b/packages.mail @@ -1,4 +1,3 @@ -abook bash crawl fortune-mod @@ -11,7 +10,6 @@ mutt nethack readline reply -screen sh ssh starship diff --git a/packages.mz-doy1 b/packages.mz-doy1 index 4f276e0..5bd0864 100644 --- a/packages.mz-doy1 +++ b/packages.mz-doy1 @@ -13,7 +13,6 @@ local readline redshift reply -screen sh starship tig diff --git a/packages.partofme b/packages.partofme index e4cae92..9edbc1a 100644 --- a/packages.partofme +++ b/packages.partofme @@ -1,4 +1,3 @@ -abook bash crawl fortune-mod @@ -11,7 +10,6 @@ mutt nethack readline reply -screen sh ssh starship diff --git a/packages.root b/packages.root index d049884..9b528c5 100644 --- a/packages.root +++ b/packages.root @@ -7,7 +7,6 @@ less local readline reply -screen sh ssh starship diff --git a/packages.tozt b/packages.tozt index e4cae92..9edbc1a 100644 --- a/packages.tozt +++ b/packages.tozt @@ -1,4 +1,3 @@ -abook bash crawl fortune-mod @@ -11,7 +10,6 @@ mutt nethack readline reply -screen sh ssh starship diff --git a/screen/.screenrc b/screen/.screenrc deleted file mode 100644 index 414b4c9..0000000 --- a/screen/.screenrc +++ /dev/null @@ -1,28 +0,0 @@ -# .screenrc, based on Eidolos' config at http://sartak.org/conf/ -startup_message off -nethack on - -# bells in background windows make bells in the foreground window -bell_msg "^G" - -# i turn visual bell on in terminal settings, don't do it here -vbell off - -# avoid logging in a new user for every term (thanks toft) -deflogin off - -# lines of scrollback! -defscrollback 4096 - -# quick esc timeout -maptimeout 50 - -# use the alternate screen feature (for full-screen curses apps, etc) -altscreen on - -# some keybindings -bind ^a windowlist -b -bind ^g screen //group - -# i want utf8 in screen too -defutf8 on -- cgit v1.2.3-54-g00ecf