Skip to content

Commit

Permalink
Initial version
Browse files Browse the repository at this point in the history
  • Loading branch information
da-x committed Apr 21, 2015
0 parents commit 60d5da4
Show file tree
Hide file tree
Showing 6 changed files with 652 additions and 0 deletions.
24 changes: 24 additions & 0 deletions LICENSE
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
Copyright (c) 2015, Dan Aloni
All rights reserved.

Redistribution and use in source and binary forms, with or without
modification, are permitted provided that the following conditions are met:

* Redistributions of source code must retain the above copyright notice, this
list of conditions and the following disclaimer.

* Redistributions in binary form must reproduce the above copyright notice,
this list of conditions and the following disclaimer in the documentation
and/or other materials provided with the distribution.

THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.

123 changes: 123 additions & 0 deletions README.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,123 @@
About - git-bottle v0.1-rc
--------------------------

git-bottle is an **experimental** utility for the purpose of saving and restoring the various ``git`` working states *as normal git commits*, effectively snapshotting the current and pertinent state of your working tree and various file states shown under ``git status``:

* Modified but not staged
* Staged but not commited
* Untracked files that are not ignored via ``.gitignore``
* Unmerged paths (in all the various states - rebase, cherry-pick, or merge)
* Rebase state
* Rebase-interactive state
* Merge state

To illustrate how the commit history looks like when using ``git-bottle``. Suppose that we have a complex state during a merge:

.. code-block:: console
$ git status
On branch master
Your branch and 'origin/master' have diverged,
and have 3 and 1 different commit each, respectively.
(use "git pull" to merge the remote branch into yours)
You have unmerged paths.
(fix conflicts and run "git commit")
Changes to be committed:
modified: file-non-conflicting-2
Unmerged paths:
(use "git add/rm <file>..." as appropriate to mark resolution)
both modified: file
deleted by us: file-will-be-removed-down
deleted by them: file-will-be-removed-up
Changes not staged for commit:
(use "git add <file>..." to update what will be committed)
(use "git checkout -- <file>..." to discard changes in working directory)
modified: file-non-conflicting-2
modified: file-non-conflicting-3
Untracked files:
(use "git add <file>..." to include in what will be committed)
untracked
..

But we decide to put this merge aside for some reason. We use git-bottle, and the result is:

.. code-block:: console
$ git-bottle
<some various prints here>
$ git status
On branch master
Your branch is ahead of 'origin/master' by 4 commits.
(use "git push" to publish your local commits)
nothing to commit, working directory clean
$ git-log --oneline --stat
3251e3b1cf3 (HEAD, master) GIT_BOTTLE_STATE: untracked
untracked | 3 +++
1 file changed, 3 insertions(+)
1075c68e8448 GIT_BOTTLE_STATE: uncommitted
file-non-conflicting-2 | 1 +
file-non-conflicting-3 | 1 +
2 files changed, 2 insertions(+)
f7f6727daaa3 GIT_BOTTLE_STATE: staged
file-non-conflicting-2 | 1 +
1 file changed, 1 insertion(+)
12992d33063b GIT_BOTTLE_STATE_MERGE: Merge remote-tracking branch 'origin/master'
.GIT_BOTTLE_MERGE_HEAD | 1 +
.GIT_BOTTLE_MERGE_MODE | 0
.GIT_BOTTLE_MERGE_MSG | 6 ++++++
file | 5 +++++
file-will-be-removed-down | 2 ++
file-will-be-removed-down.1.GIT_BOTTLE_UNMERGED_NORMALIZED | 1 +
file-will-be-removed-down.3.GIT_BOTTLE_UNMERGED_NORMALIZED | 2 ++
file-will-be-removed-up.1.GIT_BOTTLE_UNMERGED_NORMALIZED | 1 +
file-will-be-removed-up.2.GIT_BOTTLE_UNMERGED_NORMALIZED | 2 ++
file.1.GIT_BOTTLE_UNMERGED_NORMALIZED | 1 +
file.2.GIT_BOTTLE_UNMERGED_NORMALIZED | 2 ++
file.3.GIT_BOTTLE_UNMERGED_NORMALIZED | 2 ++
12 files changed, 25 insertions(+)
..
The branch now contains the needed data to restore both the working tree, index, and the meta-data regarding the merge. If we run ``git-unbottle``, we would find outselves back in the complex ``git status`` from above. The special commit messages assist ``git-unbottle`` in the work of unwinding the effects of ``git-bottle``.

Main use case
~~~~~~~~~~~~~

You find yourself working on resolving conflicts in a big merge or a rebase, but some of the conflicts are in other people's code. Those people are not around, or temporarily unavailble. You want to pass the torch of conflict resolution to the other person and shift to work on more interesting things. You are in the midst on the merge, 12 out of 30 files done, meaning you neither ``git-stash`` or a simple ``git-commit`` would get you out of that state. Well, you can work from another clone of the project, but your Eclipse or another IDE was rigged so perfectly to function properly from that single clone path.

``git-bottle`` to the rescue - you bottle up the current unfinished merge, push it to say, by convention e.g. ``bottle/whatever-branch``, and notify the other person. The other guy will do ``git-fetch``, checkout that branch, perform ``git-unbottle``, and resume the merge.

Conflict resolution in a separate commit
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

When merging, one might want to leave the merge commit as it is and solve the conflicts in a subsequent commit. This way, the conflict resolution is made more explict, cherry-pickable, etc. ``git-bottle`` facilitates with this approach.

A better stash
~~~~~~~~~~~~~~

Never forget again that you have made stashes. Instead, with ``git-bottle``, they could be local branches lying around. Hopefully, this demonstrates that by turning the index and many other working states into the first-class citizen ``commit object``, the flexibility of ``git`` can be improved.


Installation
------------

The ``git-*`` scripts from this repository need to be somewhere in ``$PATH`` for the commands to work.

**Big fat warning**
-------------------

* ``git-bottle`` is sensitive to the versions of git program used because it saves some of the meta-data, especially during rebase and rebase-interactive. It might break if there is a mismatch of versions of the git program. I have tested it over Git 2.1.0.
* Be careful with ``git-unbottle`` of states from untrusted sources! The state of rebase-interactive might contain bash scripts.
* For devs - this is unfinished work. I am sorry if you find the code undocumented/unclear/buggy, but I'd consider every pull request.
151 changes: 151 additions & 0 deletions git-bottle
Original file line number Diff line number Diff line change
@@ -0,0 +1,151 @@
#!/bin/bash -ue

# Utility functions
cur_tree() {
git show --format="%T" -s
}

# find normalization script
NORMALIZATION_SCRIPT=git-unmerged-normalization
set +e
which ${NORMALIZATION_SCRIPT} 2>/dev/null 1>/dev/null
if [ "0" != "$?" ] ; then
set -e
NORMALIZATION_SCRIPT=`dirname ${BASH_SOURCE}`/${NORMALIZATION_SCRIPT}
else
set -e
NORMALIZATION_SCRIPT=`which ${NORMALIZATION_SCRIPT}`
fi

if [ ! -x ${NORMALIZATION_SCRIPT} ] ; then
echo ${NORMALIZATION_SCRIPT} not found
exit -1
fi

bottle() {
# Turn unmerged paths into a commit. Unless we do this
# git likes to complain about the state of the index. It's
# normallization 'normallization' of the index.

${NORMALIZATION_SCRIPT} encode

git_top=`git rev-parse --show-toplevel`/.git

# Turn staged files into a commit
tree=`cur_tree`
git commit -m "GIT_BOTTLE_STATE: staged" --allow-empty > /dev/null
if [ "${tree}" == "`cur_tree`" ] ; then
# Remove empty commit
git reset --soft HEAD^
else
echo "git-bottle: commit created - staged changes"
fi

# Turn modified but unadded files into a commit
tree=`cur_tree`
git commit -a -m "GIT_BOTTLE_STATE: uncommitted" --allow-empty > /dev/null
if [ "${tree}" == "`cur_tree`" ] ; then
# Remove empty commit
git reset --soft HEAD^
else
echo "git-bottle: commit created - unstaged changes to tracked files"
fi

# Turn untracked but not ignored files a commit
tree=`cur_tree`
git add -A .
git commit -m "GIT_BOTTLE_STATE: untracked" --allow-empty > /dev/null
if [ "${tree}" == "`cur_tree`" ] ; then
# Remove empty commit
git reset --soft HEAD^
else
echo "git-bottle: commit created - untracked files"
fi

commit_special_state() {
state=$1
branch=`cat ${git_top}/${state}/head-name`
branch=${branch#refs/heads/}
gitversion=`git --version`

echo Detected ${state}
mv ${git_top}/${state} .GIT-BOTTLE-STATE-${state}

git add -f .GIT-BOTTLE-STATE-${state}

echo -e "GIT_BOTTLE_STATE: ${state}\n" > ${git_top}/TEMP_MSG
echo "Branch: ${branch}" >> ${git_top}/TEMP_MSG
echo "Git-Version: ${gitversion}" >> ${git_top}/TEMP_MSG

git commit -F ${git_top}/TEMP_MSG --allow-empty
rm -f ${git_top}/TEMP_MSG
rev=$(git rev-parse HEAD)
git checkout ${branch} 2>/dev/null >/dev/null
git reset --hard ${rev}
}

# Turn various special states into a commit
if [[ -f ${git_top}/rebase-merge/head-name ]] ; then
commit_special_state rebase-merge
elif [[ -f ${git_top}/rebase-apply/head-name ]] ; then
commit_special_state rebase-apply
fi
}

unbottle() {
git_top=`git rev-parse --show-toplevel`/.git

subj=`git show HEAD --format="%s" -s`
if [ "${subj}" == "GIT_BOTTLE_STATE: rebase-merge" ] ; then
echo Detected rebase-merge
git checkout `git rev-parse HEAD` 2>/dev/null
git reset --soft HEAD^
git reset HEAD .
mv .GIT-BOTTLE-STATE-rebase-merge ${git_top}/rebase-merge
elif [ "${subj}" == "GIT_BOTTLE_STATE: rebase-apply" ] ; then
echo Detected rebase-apply
git checkout `git rev-parse HEAD` 2>/dev/null
git reset --soft HEAD^
git reset HEAD .
mv .GIT-BOTTLE-STATE-rebase-apply ${git_top}/rebase-apply
fi

subj=`git show HEAD --format="%s" -s`
if [ "${subj}" == "GIT_BOTTLE_STATE: untracked" ] ; then
git reset --soft HEAD^
git reset HEAD .
fi

subj=`git show HEAD --format="%s" -s`
if [ "${subj}" == "GIT_BOTTLE_STATE: uncommitted" ] ; then
git reset --soft HEAD^
git reset HEAD .
fi

subj=`git show HEAD --format="%s" -s`
if [ "${subj}" == "GIT_BOTTLE_STATE: staged" ] ; then
git reset --soft HEAD^
fi

${NORMALIZATION_SCRIPT} decode
}

cmd=bottle

while getopts "uh" opt; do
case $opt in
u)
cmd=unbottle
;;
h)
echo "${BASH_SOURCE} [-u]"
exit 1
;;
\?)
echo "invalid option ${opt}"
exit 1
;;
esac
done

${cmd} "$@"
3 changes: 3 additions & 0 deletions git-unbottle
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
#!/bin/bash -ue

`dirname ${BASH_SOURCE}`/git-bottle -u "$@"
Loading

0 comments on commit 60d5da4

Please sign in to comment.