├── .gitignore ├── Formula └── ib-unfuck-git.rb ├── LICENSE ├── README.md ├── scripts └── ibunfuck ├── setup.py └── src ├── __init__.py ├── ibunfuck.py └── plugins.py /.gitignore: -------------------------------------------------------------------------------- 1 | *.py[cod] 2 | 3 | # Packages 4 | *.egg 5 | *.egg-info 6 | dist 7 | build 8 | eggs 9 | parts 10 | bin 11 | var 12 | sdist 13 | develop-eggs 14 | .installed.cfg 15 | lib 16 | lib64 17 | 18 | venv/ 19 | -------------------------------------------------------------------------------- /Formula/ib-unfuck-git.rb: -------------------------------------------------------------------------------- 1 | class IbUnfuckGit < Formula 2 | desc "Removes unnecessary changes from iOS/OSX repositories" 3 | homepage "https://github.com/Reflejo/ib-unfuck-git" 4 | url "https://github.com/Reflejo/ib-unfuck-git/archive/v0.3.tar.gz" 5 | 6 | depends_on :python if MacOS.version <= :snow_leopard 7 | 8 | resource "gitpython" do 9 | url "https://pypi.python.org/packages/source/G/GitPython/GitPython-1.0.1.tar.gz" 10 | sha256 "9c88c17bbcae2a445ff64024ef13526224f70e35e38c33416be5ceb56ca7f760" 11 | end 12 | 13 | resource "lxml" do 14 | url "https://pypi.python.org/packages/source/l/lxml/lxml-3.5.0.tar.gz" 15 | sha256 "349f93e3a4b09cc59418854ab8013d027d246757c51744bf20069bc89016f578" 16 | end 17 | 18 | resource "unidiff" do 19 | url "https://pypi.python.org/packages/source/u/unidiff/unidiff-0.5.1.tar.gz" 20 | sha256 "c3d52b3656044c90af6cd01b3424d21d669e99899f1bdde82cc4bbd3fa5fda67" 21 | end 22 | 23 | resource "gitdb" do 24 | url "https://pypi.python.org/packages/source/g/gitdb/gitdb-0.6.4.tar.gz" 25 | sha256 "a3ebbc27be035a2e874ed904df516e35f4a29a778a764385de09de9e0f139658" 26 | end 27 | 28 | resource "smmap" do 29 | url "https://pypi.python.org/packages/source/s/smmap/smmap-0.9.0.tar.gz" 30 | sha256 "0e2b62b497bd5f0afebc002eda4d90df9d209c30ef257e8673c90a6b5c119d62" 31 | end 32 | 33 | def install 34 | ENV.prepend_create_path "PYTHONPATH", libexec/"vendor/lib/python2.7/site-packages" 35 | %w[gitdb smmap unidiff gitpython lxml].each do |r| 36 | resource(r).stage do 37 | system "python", *Language::Python.setup_install_args(libexec/"vendor") 38 | end 39 | end 40 | 41 | ENV.prepend_create_path "PYTHONPATH", libexec/"lib/python2.7/site-packages" 42 | system "python", *Language::Python.setup_install_args(libexec) 43 | 44 | bin.install Dir[libexec/"bin/*"] 45 | bin.env_script_all_files(libexec/"bin", :PYTHONPATH => ENV["PYTHONPATH"]) 46 | end 47 | 48 | test do 49 | end 50 | end 51 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | This is free and unencumbered software released into the public domain. 2 | 3 | Anyone is free to copy, modify, publish, use, compile, sell, or 4 | distribute this software, either in source code form or as a compiled 5 | binary, for any purpose, commercial or non-commercial, and by any 6 | means. 7 | 8 | In jurisdictions that recognize copyright laws, the author or authors 9 | of this software dedicate any and all copyright interest in the 10 | software to the public domain. We make this dedication for the benefit 11 | of the public at large and to the detriment of our heirs and 12 | successors. We intend this dedication to be an overt act of 13 | relinquishment in perpetuity of all present and future rights to this 14 | software under copyright law. 15 | 16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 17 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 18 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. 19 | IN NO EVENT SHALL THE AUTHORS BE LIABLE FOR ANY CLAIM, DAMAGES OR 20 | OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, 21 | ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR 22 | OTHER DEALINGS IN THE SOFTWARE. 23 | 24 | For more information, please refer to 25 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | IB Unfuck (GIT) 2 | =============== 3 | 4 | This script will revert all the unneeded changes caused by Interface Builder. For example `` tags, `+-1` changes on ``, ``, ``. 5 | 6 | 7 | 8 | Install 9 | ------- 10 | 11 | ```bash 12 | $ brew install https://raw.githubusercontent.com/Reflejo/ib-unfuck-git/master/Formula/ib-unfuck-git.rb 13 | ``` 14 | 15 | Usage 16 | ------- 17 | 18 | Important: Make sure your changes are still in unstaged state only then this will remove the unwanted changes. 19 | 20 | Run `ibunfuck` command from your repository directory to remove the unwanted changes. -------------------------------------------------------------------------------- /scripts/ibunfuck: -------------------------------------------------------------------------------- 1 | #!/usr/local/bin/python 2 | 3 | import git 4 | import sys 5 | 6 | from ibunfuck import UnfuckPatch 7 | 8 | 9 | def main(): 10 | repository_path = sys.argv[1] if len(sys.argv) == 2 else "." 11 | try: 12 | unfuck = UnfuckPatch(repository_path) 13 | except git.exc.InvalidGitRepositoryError: 14 | print "Error: Current path is not a git repository\n" 15 | print "Usage: %s " % sys.argv[0] 16 | 17 | unfuck.clear() 18 | 19 | 20 | if __name__ == "__main__": 21 | main() 22 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | from setuptools import setup, find_packages 2 | from codecs import open 3 | from os import path, chdir 4 | 5 | here = path.abspath(path.dirname(__file__)) 6 | 7 | chdir(path.join(here, "src")) 8 | 9 | # Get the long description from the README file 10 | with open(path.join(here, 'README.md'), encoding='utf-8') as f: 11 | long_description = f.read() 12 | 13 | setup( 14 | name='ibunfuck', 15 | version='0.1', 16 | description='Removes unnecessary changes from iOS/OSX repositories', 17 | url='https://github.com/Reflejo/ib-unfuck-git', 18 | author='Martin Conte Mac Donell', 19 | author_email='Reflejo@gmail.com', 20 | license='MIT', 21 | install_requires=['unidiff', 'gitpython<2', 'lxml'], 22 | packages=["."], 23 | scripts=['../scripts/ibunfuck'] 24 | ) 25 | -------------------------------------------------------------------------------- /src/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Reflejo/ib-unfuck-git/6f23b9807a63efcdf129e96d2a0a3b70648cb610/src/__init__.py -------------------------------------------------------------------------------- /src/ibunfuck.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python 2 | 3 | import git 4 | import sys 5 | import tempfile 6 | import unidiff 7 | 8 | from io import StringIO 9 | from plugins import IBPlugin 10 | 11 | 12 | class UnfuckPatch(object): 13 | """ 14 | Contains the logic to call plugins that reverts unnecessary changes 15 | to the repository. 16 | 17 | >>> unfuck = UnfuckPatch(".") 18 | >>> unfuck.clear() 19 | """ 20 | default_processors = [ 21 | IBPlugin.process_rect, IBPlugin.process_size, IBPlugin.process_point, 22 | IBPlugin.process_animations 23 | ] 24 | 25 | def __init__(self, path): 26 | self.repository = git.Repo(path) 27 | 28 | def _clear_patch(self, patch, processors): 29 | has_changes = False 30 | for i, patch_piece in enumerate(patch): 31 | length = len(patch_piece) 32 | for j, hunk in enumerate(patch_piece[::-1]): 33 | if not all(p(hunk) for p in processors): 34 | continue 35 | 36 | del patch[i][length - j - 1] 37 | 38 | has_changes = has_changes or len(patch[i]) > 0 39 | 40 | return has_changes 41 | 42 | def clear(self, processors=None): 43 | """ 44 | Starts the process of cleaning unnessesary changes using given 45 | processors if no processor is given, we'll use the default ones. 46 | 47 | Processors are functions that receive a hunk and return 48 | `True` or `False`, when any processor returns `False`, the hunk is 49 | reverted from the working tree. 50 | """ 51 | processors = processors or self.default_processors 52 | index = self.repository.index 53 | patches = index.diff(None, create_patch=True, unified=0) 54 | for patch in patches: 55 | try: 56 | patch = unidiff.PatchSet(StringIO(patch.diff.decode('utf-8'))) 57 | except Exception as e: 58 | print("Unhandled error %s, continuing..." % str(e)) 59 | continue 60 | 61 | if self._clear_patch(patch, processors): 62 | patchpath = tempfile.mktemp() 63 | open(patchpath, 'w').write(str(patch) + '\n') 64 | self.repository.git.execute( 65 | ['git', 'apply', '--recount', '-R', '--unidiff-zero', 66 | '--allow-overlap', patchpath] 67 | ) 68 | 69 | 70 | def main(): 71 | repository_path = sys.argv[1] if len(sys.argv) == 2 else "." 72 | try: 73 | unfuck = UnfuckPatch(repository_path) 74 | except git.exc.InvalidGitRepositoryError: 75 | print("Error: Current path is not a git repository\n") 76 | print("Usage: %s " % sys.argv[0]) 77 | 78 | unfuck.clear() 79 | 80 | 81 | if __name__ == "__main__": 82 | main() 83 | -------------------------------------------------------------------------------- /src/plugins.py: -------------------------------------------------------------------------------- 1 | from lxml import etree 2 | 3 | 4 | class IBPlugin(object): 5 | """ 6 | These processors will mark unnecessary xcode changes as invalid. 7 | 8 | Example: +-1 on rect values, `` tags, etc. 9 | """ 10 | 11 | @classmethod 12 | def _xml_changes(klass, chunk): 13 | # Returns two arrays one for xml additions and other for removals 14 | try: 15 | plus = [etree.fromstring(str(line)[1:]) 16 | for line in chunk if line.is_removed] 17 | minus = [etree.fromstring(str(line)[1:]) 18 | for line in chunk if line.is_added] 19 | return plus, minus 20 | except etree.XMLSyntaxError: 21 | return [], [] 22 | 23 | @classmethod 24 | def _is_valid_dimension(klass, hunk, tag, properties): 25 | minus, plus = klass._xml_changes(hunk) 26 | if any(x.tag != tag for x in minus + plus): 27 | return True 28 | 29 | if len(minus) == 1 and len(plus) == 1: 30 | mnode, pnode = minus[0], plus[0] 31 | if mnode.tag != pnode.tag or mnode.tag != tag or pnode.tag != tag: 32 | return True 33 | 34 | is_right_tag = mnode.tag == pnode.tag == tag 35 | diffs = [abs(float(mnode.attrib[p]) - float(pnode.attrib[p])) 36 | for p in properties] 37 | return not is_right_tag or max(diffs) > 1.0 38 | 39 | return True 40 | 41 | @classmethod 42 | def process_animations(klass, hunk): 43 | """ 44 | Marks all `` changes as invalid. 45 | """ 46 | minus, plus = klass._xml_changes(hunk) 47 | return (minus or not plus or 48 | not all(n.tag == "animations" for n in plus)) 49 | 50 | @classmethod 51 | def process_point(klass, hunk): 52 | """ 53 | Marks all +- 1 changes on tags invalid. 54 | """ 55 | return klass._is_valid_dimension(hunk, "point", ['x', 'y']) 56 | 57 | @classmethod 58 | def process_size(klass, hunk): 59 | """ 60 | Marks all +- 1 changes on tags invalid. 61 | """ 62 | return klass._is_valid_dimension(hunk, "size", ['width', 'height']) 63 | 64 | @classmethod 65 | def process_rect(klass, hunk): 66 | """ 67 | Marks all +- 1 changes on tags invalid. 68 | """ 69 | return klass._is_valid_dimension(hunk, "rect", 70 | ['x', 'y', 'width', 'height']) 71 | --------------------------------------------------------------------------------