Reputation: 3581
I'm a programmer who's more familiar with higher-level programming languages such as C# or Python, but I've recently started working deeper into the intricacies of Git. Context: I'm developing a game, and I have a development
branch with a specific 'Development'
directory which will not be pushed to master
in a merge, for the sake of keeping master
clean.
I came to this answer which nearly does the job; however, this method, while it won't commit the changes to the directory, still creates it on the master
branch, meaning that after every merge, I would still need to go back and delete the files manually.
I'm setting up an alias git publish
to automate this task, which I'll post here for completeness:
[alias]
publish = "!f(){ git checkout master \
&& git merge --no-commit --no-ff development \
&& git reset -- Assets/Development/ \
&& git commit \
&& git push; };f"
I'd like to know, is there any way I can prevent Assets/Development/
from being created on master
?
Upvotes: 1
Views: 418
Reputation: 489083
Side note: fiddling with .gitignore
won't help, because of the nature of .gitignore
, which is not really "files to ignore", but rather "files to suppress certain complaints about, and/or to feel free to destroy in some cases". (This is not meant to say that you should not adjust .gitignore
for other reasons, just that it won't help with this merge issue, because you really do want those files versioned in the development branch.)
One trick is basically what you are doing now: do the merge (with --no-commit
), then strip out the stuff you did not want.
The strip-out step is this:
git reset -- Assets/Development/
This uses the --mixed
(default, and forced upon you when giving paths) form of git reset
, which sets (or "resets", really) the index to match the specified tree for the specified path(s). Here the path is Assests/Development/
, i.e., everything within that subdirectory. The specified tree is, well, not specified—so it defaults to HEAD
, or rather, the tree for HEAD
. HEAD
is the current commit on the current branch, which is of course master
since we just checked that out; and master
does not contain Assets/Development/
at all, so the re-set index now no longer has that sub-tree.
In short, because Assets/Development/
is not in HEAD
, this undoes the git add
s that git merge
did on everything in Assets/Development
. The problem is that while this removes the entries from the index, it leaves them in the work tree.
One might try git reset --hard -- Assets/Development/
but git won't do that: you cannot specify a reset mode when giving paths. That leaves three more obvious options, though:
add rm -r Assets/Development
(just before or just after the git reset
, or even after the commit
step)
This removes the files, so with them gone from the index and work-tree, you can commit and be done.
use git rm -rf Assets/Development/
(in place of the git reset
)
This removes the index entries and removes the work-tree files (and directory), so you can commit and be done.
use git clean
after committing. This may do more than you want.
The main thing is, you may only have to do this once, not repeatedly. It depends on what changes you make along the way.
When you run git merge <commit>
, git first finds the merge base.
The merge base is defined as the Lowest Common Ancestor (LCA) in the commit graph between two nodes in the graph. The two nodes are your current commit—in this case, the tip of branch master
—and the commit you give to git merge
, which in this case is the tip commit of branch development
. The definition of LCA requires a bit of graph theory, but loosely speaking, it's the "nearest" commit that is on both commit histories.
To see how this works, it helps to draw part of the commit graph. Here we have master
and development
as two branch tips, with the two branch structures (chains of commits) eventually joining up:
... <- o <- * <- o <- ... <- o <-- master
\
o <- o <- ... <- o <-- development
The commit marked *
is the merge base: it's the point where development
and master
rejoin, and commit *
and every earlier commit is on both branches, rather than just being on one.
What merge
does at this point is to generate two diffs: one from *
to the tip of the current commit (the tip commit on master
), and one from *
to the other, target, commit (the tip of development
).
In master
you have some (probably not too many) changes since *
, and in development
you have a bunch of changes, including the addition of Assets/Development/
.
The merge
command combines the changes, which include adding the unwanted directory. When all done, git makes a new commit (the --no-ff
step guarantees this; if the merge base commit *
were the tip of master
, which is not what we drew but is possible on the first merge, git would try to do a label fast-forward instead of making an actual merge).
The result is a graph that now looks more like this (I'm going to stop drawing in the left-pointing arrows, just be aware that history goes leftward, i.e., later commits point back to earlier ones):
...--o--*--o-...-o---o <-- master
\ /
o--o-...-o <-- development
Now you continue to develop on development
, making more new commits. Let's replace *
with o
since it's no longer a special case.
...--o--o--o-...-o---o <-- master
\ /
o--o-...-o--o--o <-- development
and at some point you decide to merge again, so you check out master
and tell git merge
to find the (new) tip of development
and calculate the merge base.
The merge base is, again (and a bit loosely), the first commit that's on both branches. Let's find it: is it the same as before? Start at the tips of both master
and development
and work backwards from each, coloring nodes with two different colors. As soon as a node gets both colors, that's a common ancestor. The one closest to the tips is the "lowest", i.e., is the merge base.
I can't draw colors, but if you do the exercise yourself, you will see that it's this node:
...--o--o--o-...-o---o <-- master
\ /
o--o-...-*--o--o <-- development
The node now marked *
is reachable from master
(by following the second parent down-and-left), and from development
(by following the chain left). It is the closest such node, so it is the merge base.
Git now diffs the merge base *
against the two tips. There should be few if any changes to the tip of master
(basically only whatever was picked up from the o
nodes between the previous merge base and the new tip of master
), and whatever changes you have made to the development
branch since the last merge.
If those changes affect existing files in Assets/Development
, those changes will cause a merge conflict:
CONFLICT (modify/delete): Assets/Development/foo deleted in HEAD
and modified in development. Version development of
Assets/Development/foo left in tree.
Automatic merge failed; fix conflicts and then commit the result.
Note that you don't need the --no-commit
for these.
If those changes add new files in Assets/Development/
, on the other hand, merge will happily add them to the commit you are about to make, so you do need --no-commit
to avoid adding any such files.
You can again use git rm -rf
to just toss any conflicts and/or added files, if necessary.
Upvotes: 1