1

I have a branch develop, and branched a big feature called next from it.

In next there is a continuous process of editing files that are originally from develop, and files that are created new exclusively in next.

TL;DR: You can skip the next section and go to the question directly.


(a) The changes in next on files that were inherited from develop are always backwards-compatible and are always committed there (too).

(b) Meanwhile, develop itself is also undergoing development, and these changes should also be visible in next. Therefore, next is rebased on develop every now and then.

Committing back to develop causes merge conflicts because (b), and rebasing causes merge conflicts because (a). The latter can be fixed by -X theirs because the conflicts were already resolved in the first step.

But this is not efficient.


Is there a way to automatically:

  1. Commit files that were originally from a parent branch to the parent branch (e.g. develop)
  2. Commit files that do not exist on the parent branch to the current branch (e.g. next)
  3. Rebase the current branch upon the parent branch to bubble up the changes?

Possible complex solution

I can imagine that some 'simple'(r) git-foo is possible because this wish is not unique. But if not, some bash scripting might be possible. For example, the following command:

git ls-tree -r develop --name-only

will show a list of files that were indeed in develop. Files not in that list would be only in next.

I guess if all else fails, I could create a shellscript to:

  1. First commit all files that exist on develop to develop
  2. Then commit all other files to next
  3. Rebase next on develop

But it's not as simple as it sounds.

Redsandro
  • 11,060
  • 13
  • 76
  • 106

3 Answers3

1

As always, I would start by recommending that you draw the (known) "before" and (desired) "after" commit graph first. Well, maybe second: first, decide whether you want to allow these operations with a "dirty work tree" and/or "dirty index" (one that does not match the current, HEAD, commit).

In this case, though, you may want to augment the graphs with secondary labeling regarding the snapshot tree objects you want in each, especially with examples of how you want to treat files that in the (existing) commit in question, vs files that may be in the index and/or work-tree at the time the whole process starts. This might help inform a decision on how (in terms of Git mechanisms and script-able commands) to proceed.

Once you have these drawn, remember, Git is a big box of tools that you may employ however you like. These tools include the following features:

  • A new commit has, as its parent, whatever commit is HEAD, and whatever is in the index at that moment as its tree. But then see also git commit -a, which will take new file versions from the work-tree for files whose paths are already in the index, or remove from the index paths that are missing from the work-tree.

    Of course, a file that is only in the index or work-tree is vulnerable to removal. Committing the existing index makes it as permanent as any commit (see below). So in general, if you have uncommitted work, the first thing to do is commit it. The new commit has the old commit as its parent, and the new commit is now in HEAD—or, usually, in the branch name to which HEAD refers (so that git rev-parse HEAD finds the new ID, but does so by meaning git rev-parse refs/heads/next or whatever).

  • Branch names just point to tip commits. Each commit then points back to its immediate parent (or for merge commits, parents, plural). A chain of commits that ends with a named tip commit is also called a branch (see What exactly do we mean by "branch"?). You can use a detached HEAD to make new commits that have no name yet, if that's convenient: you just need to save their IDs somewhere (using git rev-parse HEAD once they are made, to copy the ID into a shell variable, for instance) s so that you can name them. You must give them permanent names—usually a branch name—within the default prune expiration of two weeks to keep them from being reaped by garbage collection.

  • git stash, if you choose to use it, works by saving the current index as a commit, then saving the current work-tree as another commit, and then optionally saving untracked, or even untracked-and-ignored, files as a third commit. None of the three commits are on any branch; instead, the special name refs/stash points to one of these two-or-three commits (which then serves to find the others, and also the commit that was HEAD when git stash made both or all three). These commits do not participate in git rebase. If you require a clean work-tree and/or index, you will have no need to use git stash and its complications. If not, you may want it, or to do something like it.

  • Running git rebase --onto target limit will copy commits selected by limit..HEAD (but generally discarding merges and pre-cherry-picked commits, i.e., this really uses --cherry-pick --right-only --no-merges limit...HEAD, though the specifics vary a bit by the type of rebase as well, including -m, -i, -k, and -p arguments if given). The target defaults to the commit given as the limit argument (which the rebase documentation calls <upstream>). Usually this is appropriate and sufficient; that may or may not be true in your case, depending on how and when you go about adding commits.

How I have done this before

All that said, I've dealt with things like this myself before, and I will describe how I prefer to do it. Whether this will work for you is another question entirely.

You have a "base branch", in your case develop, that I assume a larger group is working on. You also have a feature or topic branch (which you call next but I usually give a more specific name, project-mac or whatever (though I was never at MIT myself...)). What I do is to number my branches.

  1. project/0 (or project-0, I have not been consistent): my first stab at it. This is basically git checkout -b project/0 develop, then hack and commit for a while.

  2. project/1 (or project-1): some lessons learned, I "rebase" on the current main branch, by cherry-picking or rewriting or whatever. Usually between zero and one there is not enough salvageable code to bother with a tool like git rebase.

  3. Now that some parts are sufficiently solid, I make my own commits into the main branch (and depending on the job and so on, get them reviewed or passing tests or whatever). Now I make project/2 by, in Git, doing:

    git checkout -b project/2 project/1
    git rebase develop
    

    This automatically throws out commits that are already cleanly cherry-picked into develop (by me as the first part of step 3), and gives me merge conflicts on parts that are "uncleanly cherry-picked" or otherwise conflict with other work, which I resolve. In tough cases, I can easily git log project/1 and git show specific commits from there, if I need to remember what I did and why I did it that way. If I was good about commit messages, they serve as the necessary reminders.

  4. Rinse and repeat ad nauseam.

Note that none of these branches ever actually get rebased, except during that initial step when the new numbered flavor is created from the older one. This means they can easily be shared across multiple clones, which is very useful when something must be tested on different OSes, or distributed across multiple people.

Community
  • 1
  • 1
torek
  • 448,244
  • 59
  • 642
  • 775
  • Upvoted for now. I'll be following your steps in detail at a later time. For now, could you expand on what you mean by _commit graph_? – Redsandro Feb 28 '17 at 23:52
  • See many (though by no means all) other StackOverflow postings/answers, such as http://stackoverflow.com/a/40526137/1256452 or http://stackoverflow.com/a/36049870/1256452. For another way to draw them, see http://stackoverflow.com/q/25068543/1256452 – torek Mar 01 '17 at 00:30
  • Those graphs don't make it any clearer for me, honestly. But I used part of your `stash` hint to convert the __possibly complex solution__ in my question to an exact answer, so there is no need for more branches. The trick is in getting file lists from both the tree and the parent branch, and feed a _diff_ into `git add`. – Redsandro Mar 03 '17 at 17:08
1

I understand there is no built-in way of doing this, although I would imagine this is well sought after. So I went for some bash hacking and came up with this.

Note that this only works without merge conflics when you are freshly rebased prior to making changes.


In order to separate which files should be committed to the parent (develop) branch and which files should be committed to the feature (next), these are the most important commands:

  • List all changed, deleted, (renamed) and new files:
    • git ls-files -mdo --exclude-standard
  • List all files that exist in parent branch (develop):
    • git ls-tree -r develop --name-only
  • Intersect those lists to find out which changed files exist in the parent branch:
    • sort <(git ls-tree -r develop --name-only | sort) <(git ls-files -mdo --exclude-standard | sort) | uniq -d

So what we want to do is the following:

  • Stage files we want to commit to the parent branch (develop):
    sort <(git ls-tree -r develop --name-only | sort) <(git ls-files -mdo --exclude-standard | sort) | uniq -d | xargs git add
    Note that now is the time to manually add renamed files, as only the file deletion is staged at this point.
  • Stash (1) all the things! We want to commit them to this branch (next) later. But keep the staged files.
    git stash --keep-index -u
  • Stash (2) the staged files. We want to commit them to the parent branch (develop).
    git stash
  • Checkout the parent branch (develop)
    git checkout develop
  • Pop the stash(2)
    git stash pop
  • Commit the popped stash
    git commit -am "<commit message>"
  • Checkout the feature branch (next) again.
    git checkout next
  • Pop the remaining stash(1).
    git stash pop
  • Stage all (new) files
    git add -A
    git commit -m "<commit message>"
  • Rebase this feature banch on it's parent git rebase develop

Done! Both develop and next are now updated with latest code.


If you promise not to rename files, you can safely put this into a script to automate this.

mergedowncommit.sh:

#!/usr/bin/env bash
# By Redsandro (http://www.Redsandro.com/)
# 2017-03-03

if [ -z "$3" ]; then
    echo "No commit message supplied."
    echo "Usage: mergedowncommit.sh <parent> <feature> <commit message>"
    exit 1
fi

sort <(git ls-tree -r "$1" --name-only | sort) <(git ls-files -mdo --exclude-standard | sort) | uniq -d | xargs git add
git stash --keep-index -u
git stash
git checkout "$1"
git stash pop
git commit -am "$3"
git checkout "$2"
git stash pop
git add -A
git commit -m "$3"
git rebase "$2"

if [ $? -eq 0 ]; then
    echo "Looks like it worked. :)"
else
    echo "Looks like some files were not freshly rebased prior to running this script."
    echo "Please manually solve merge conflicts, and run \"git rebase --continue\"."
fi

Use as:

mergedowncommit.sh <parent> <feature> <commit message>

E.g.:

mergedowncommit.sh develop next "This is my commit message"

You can adapt this to your workflow. Perhaps add your favorite difftool for resolving merge conflicts. I can imagine you have at least one when develop is also worked on and it contains a file with version information.


Note: Use at your own risk.

Redsandro
  • 11,060
  • 13
  • 76
  • 106
  • 1
    Thanks for this! I think I've spotted a mistake in `mergedowncommit.sh` : `git rebase develop` should be `git rebase "$2"` – Richard Sands Nov 23 '17 at 14:13
  • Wow well spotted @RichardSands! Would you make my day and be the first upvote? :D – Redsandro Nov 23 '17 at 19:47
  • 1
    haha, no worries :) spotted another one... `sort <(git ls-tree -r develop --name-only | sort) <(git ls-files -mdo --exclude-standard | sort) | uniq -d | xargs git add` – Richard Sands Dec 04 '17 at 14:49
  • @RichardSands well spotted again. I guess it's no longer a secret that I use `develop` almost exclusively, and never noticed it was still hardcoded. – Redsandro Dec 04 '17 at 20:30
0

It's going to have to be done in multiple steps. Commit to next, then rebase and/or merge to get everything back into develop. Git doesn't have the ability to do this all at once

Jim Deville
  • 10,632
  • 1
  • 37
  • 47
  • Can I selectively get only changed files that already existed on `develop` into `develop`? – Redsandro Feb 28 '17 at 15:22
  • There are some tricks, but I don't know them personally. If you know the files, you could commit just those, and cherry pick that commit into develop first, then commit the new files, rebase, and merge (should be a ff merge, use --ff-only to ensure) – Jim Deville Feb 28 '17 at 15:25