1

Sometimes during development I realise I should split up my work and commit a small fix separately - however, this is made difficult if I have work-in-progress uncommitted changes, and I am already on a branch with several commits.

Git appears to have the following options, none of which are ideal:

  1. Commit the change to the current branch. Cherry-pick it later when the oustanding changes are finished and committed.
  2. Clone a 2nd repository and copy/paste the changes in there.
  3. Stash the uncommitted changes. Switch, write the small fix, commit, push, switch back, unstash (seems long winded and requires realising the small fix needs a separate branch before writing it)

I think I would like something like:

  1. Commit "to new branch", specify base commit and branch name. This would create a new branch, commit the change(s), and optionally stay on the current branch including the outstanding changes, or switch to the new branch discarding unstaged changes.

Is there anything like this I can use? Is there a better way of doing all this?

Michael Parker
  • 7,180
  • 7
  • 30
  • 39
  • possible duplicate of [How to commit my current changes to a different branch in git](http://stackoverflow.com/questions/2944469/how-to-commit-my-current-changes-to-a-different-branch-in-git) – BENARD Patrick Mar 09 '15 at 11:48
  • 1
    Why don't you just checkout a new branch before committing the fix, push and then checkout back to dev branch – Tim Mar 09 '15 at 11:50
  • Because you can't switch with outstanding uncommitted changes if those changes may conflict with what you are switching to. – Michael Parker Mar 09 '15 at 11:55
  • For the "possible duplicate" question, the answers don't help - stashing will stash all uncommitted changes including the stuff I don't want to move. Switching branch first doesn't help as above. – Michael Parker Mar 09 '15 at 11:58
  • 1
    Git provides all the plumbing commands to achieve what you want, you would have to wrap your own logic around it though. It's doable but not trivial. – Andrew C Mar 09 '15 at 15:24
  • Am I right in thinking if I were to script this, I would need to commit, stash, switch, cherry pick, (optional push), switch back, reset, unstash? I guess I could leave the Commit off and then the script would just move the last commit. – Michael Parker Mar 09 '15 at 15:32

3 Answers3

2

I'm not sure I get your issue. You have a git repo and you're on current_branch. You've made a couple of changes and you realize that you want to commit part of them on a new branch. In that case, git branch -b new_branch will create and switch to your new branch without complaining. It's only if you switch to an existing branch that you might need to stash your changes.

If the problem is that you've made changes but want to fix a bit of your code somewhere else, then stash is the solution.

edit: From the discussion, it seems that you have a stable and a current branch and want to avoid poping the stash in stable. In that case, you probably have to commit what you want in a new branch then cherry pick this commit back in stable:

$ git checkout -b tmp_new
$ git add ... && git commit
$ git stash
$ git checkout stable
$ git checkout -b new
$ git cherry_pick tmp_new
$ git branch -d tmp_new
$ git checkout current
$ git stash pop
Francis Colas
  • 3,459
  • 2
  • 26
  • 31
  • I have clarified the question further. I am already on a branch with several commits, which should not be pushed as part of the "new small fix" branch. – Michael Parker Mar 09 '15 at 15:24
  • I'm not sure I understand fully your issue. If I got it right, you're on a branch `current` and you realize some of your current modification could be a commit to a new branch `new` starting from some other branch `stable` that is less advanced than `current`. In essence, you want to teleport some (as yet uncommitted) changes, right? In that case, you could `stash`, `checkout stable`, `checkout -b new`, `stash pop`, `add+commit`, `stash`, `checkout current`. But it's only complex because your use case is. – Francis Colas Mar 09 '15 at 16:29
  • You could as well `checkout -b tmp_new`, `commit`, `stash`. `checkout stable`, `checkout -b new`, `cherry_pick from tmp_new`, `branch -d tmp_new`, `checkout current`, `stash pop`. – Francis Colas Mar 09 '15 at 16:34
  • Correct, except some of the uncommitted changes I want to leave on the `current` branch, and stashing then popping them would cause conflicts if I pop them on `stable`. Your 2nd comment is indeed what I think I would need to script. I don't think its that rare a use case, at least how I work. – Michael Parker Mar 09 '15 at 16:50
  • Francis, if you want to offer that sequence of commands as an answer, I'll accept it - it's looking like the best so far. (I suspect I'll need to write a powershell script to do this). – Michael Parker Mar 09 '15 at 16:53
  • Also if you have any suggestions as to how to make the question clearer please let me know, I am sure other people will want this at some point! :-) – Michael Parker Mar 09 '15 at 16:54
1

So you're hot-n-heavy on some branch and you make a fairly easily separable fix to your worktree that should be committed, for a concrete example, to the current branchpoint from master.

git add --patch and a sideband index have you covered here:

git branch bugfix-62831 $(git merge-base @ master)  # commit selected changes here

bash                                            # work in an interactive subshell:

export GIT_INDEX_FILE=.git/patchworkindex       # start a(n arbitrarily-named) sideband
rm $GIT_INDEX_FILE

git read-tree bugfix-62831                      # tracking the bugfix content
git add --patch fixed.c fixed.h                 # and apply selected worktree changes

git update-ref refs/heads/bugfix-62831 $(       # update the ref with the fixed content
        git commit-tree -p bugfix-62831 -m- `git write-tree`
)

exit                                            # and we're done 

All an index is is a list of pathnames and the id of repo content that goes with them. Git's read-tree loads index entries. so when git looks at that index it sees the pathnames and content as of that commit. Then git add adds new content from the worktree and points index entries at it, and add --patch lets you construct that new content from diffs against what's already there. write-tree writes the index into the repo and prints the id for the top of it, commit-tree writes a commit pointing at that into the repo, and update-ref updates a ref to point to the commit.

jthill
  • 55,082
  • 5
  • 77
  • 137
  • Could you explain a bit more whats going on here? In particular use of read-tree, commit-tree and write-tree. I've read the docs and am none the wiser! – Michael Parker Mar 10 '15 at 12:42
1

Possible the best way to do this if you plan to rebase after is to leverage a Git failsafe mechanism. It also works without rebasing: see the end of the answer.

Shell Demonstration

(feature) (+!) $ git commit -m "final message"
[feature shasha123] final message
...
(feature) (!) $ git stash
(feature) ($) $ git checkout main
(main) ($) $ git cherry-pick shasha123
(main) ($) $ git checkout feature
(feature) ($) $ git rebase main
warning: skipped previously applied commit shasha123
hint: use --reapply-cherry-picks to include skipped commits
hint: Disable this message with "git config advice.skippedCherryPicks false"
Successfully rebased and updated refs/head/feature.
(feature) ($) $ git stash pop
(feature) (!) $

Meaning of symbols:

  • + = staged changes
  • ! = unstaged changes
  • $ = stashed changes

What happened?

  1. We committed the changes on the feature branch.
  2. We stashed our uncommitted changes and switched to the main branch.
  3. We cherry-picked the commit from the feature branch, using the SHA from git commit.
  4. We switched back to the feature branch and rebased on main.
  5. Git prevented us from applying the commit twice, effectively moving the commit to the other branch.
  6. We popped the stash to get back to the desired state.

What if I don't want to rebase?

You can still use this method! Instead of rebasing, remove the commit from the feature branch using git reset --soft HEAD^ or git reset --hard HEAD^, depending on whether you want to keep the changes in this branch after they were moved to the other branch (soft if you do, hard if you don't).