├── .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 |
--------------------------------------------------------------------------------