git: how do I merge between branches while keeping

2019-02-01 11:37发布

问题:

There's a special place in hell for people who hardcode absolute paths and database credentials into multiple random places in web applications. Sadly, before they go to hell they're wreaking havoc on Earth. And we have to deal with their code.

I have to perform a few small changes to one of such web applications. I create a new branch features, and perform a global find & replace to update the paths and credentials to my local environment. I commit that. I also tag this as local.

I merrily leap into perilous hacking penitence, and after a perplexing hundred patches, I want to merge my features changes into the master branch, but I do not want the one local commit to be merged.

Onwards, I'll be merging back and forth between master and features, and I'd like local to stay put in features, and never ever show up in master.

Ideally, I'd like all this to happen magically, with as little funny parameters and whatnot as possible.

Is there a simple obvious way to do it that I'm missing?

I can think of a couple, but they all require me to remember that I don't want that commit. And that's definitely not my forte. Especially with such poorly hacked programs.

Failing that, I'm interested in more convoluted, manual-ish ways to handle the situation.

回答1:

My solution to this problem uses rebase rather than merge

Starting with a commit tree like this:

a-b-c <-- master
 \
  d <-- local
   \
    e-f-g <-- dev

$ git rebase --onto master local dev

       master 
       V 
   a-b-c-e'-f'-g' <-- dev
     \
      d <-- local

$ git checkout master

$ git merge dev

               master 
               V 
   a-b-c-e'-f'-g' <-- dev
     \
      d <-- local

$ git rebase --onto master master local

               master 
               V 
   a-b-c-e'-f'-g' <-- dev
                \
                 d' <-- local

$ git branch -f dev local

               master 
               V 
   a-b-c-e'-f'-g'
                \
                 d' <-- local
                 ^
                 dev


回答2:

You can use git cherry pick to only merge the patches you select. Just cherry pick every commit except for the local one over to the master branch.



回答3:

A technical (Git) solution would be using git attributes, using the attribute merge.

merge

The attribute merge affects how three versions of a file is merged when a file-level merge is necessary during git merge.

You will find in the SO question "How do I tell git to always select my local version for conflicted merges on a specific file?" an example of using such an attribute, to force keeping the local version of certain files when merging to a given branch.

The problem with setting merge attributes is that the files that contain the paths may contain other changed code, which I want merged

Do not forget you can associate any kind of script to manage those merges through git attributes. That include a script able to keep changes you want local, while merging the rest. It is more complicated to write such a "merge manager", but it is a way toward an ad-hoc automated solution.


A less-technical solution would be to separate the configuration values from the configuration files:

  • the configuration file contains only names to be replaced
  • the configuration values are several files (one per environment) with the actual values for each name.

A script is used to replace the name in the actual config file by the values of one of the config values files needed for a given environment.



回答4:

ok. this is not guaranteed to work every time but something like this can work (and in the cases it wont you will have a conflicting changes anyway that has to be resolved):

  • do your local branch
  • do local-only change
  • continue development

when doing merge to the master:

  • rebase -i master from your branch and move the local-only change to the END of the patch chain.
  • resolve any conflicts in the process. If the local-only change is in the config files and you are not touching them in the regular development, then you will have no problems. If, otherwise, you do have a conflict, then this is a case when you actually change in the same area and it needs your attention to resolve anyway.
  • check out master
  • merge your local-branch -1:

    git merge local^

This will leave you with master having all the changes on the local except for the last one.

If you have multiple local=only changes, I suggest you squash them together during rebase.



回答5:

Personally, if I had to do something like this and was for whatever reason prevented from refactoring credentials as I go, I'd add two more branches, ending up with an arrangement similar to the following:

master: the original code you inherited

localcred: branch from master, and add just the one patch that changes all the credentials to what you need locally. Treat this branch as read-only hereafter (and possibly add a hook to prevent accidental commits).

feature: branch from master, and all fixes go here (and possibly add a hook to prevent merging with the patch in localcred)

local: a branch (not a tag!) that will start out as a branch of localcred, and then merge feature whenever you need to run your unit tests. All testing happens from here, but no development happens here. In addition, this branch is disposable, because you might want to rebase inside of feature, and the fastest way to deal with the result will be to delete branch local, branch it again from localcred and merge feature before running your tests. This is likely to be a common enough operation in my workflow that I'd build an alias to do it repeatedly in just a few keystrokes, but I work the hell out of the disposability of Git branches, which kind of freaks out some people who watch me, so YMMV.

When you think your fixes are ready for publication, you do your final rebase of feature to clean up the history, dump and recreate local for your final test, merge feature into master, and once that's accepted upstream, merge master into localcred and rebase your credential patch to the top, then dump and recreate local and feature and play the game all over again.

If you want to rapidly test a large set of tiny variations of code without having to commit and merge each time, checkout local, make your changes until you're happy, commit, and immediately cherry-pick from local into feature, then drop and recreate local.

Does that satisfy your needs?



回答6:

I would do an interactive rebase against master and move your path-name-fixup-commit to the end. Then, you can merge up to the that point. Just keep moving your special commit to the end.

You may also find the stash useful. Instead of actually committing the path name fixups you could stash them away. If try this approach you may want to check out the question on How to reverse apply a stash.



回答7:

Well, because no answer so far provided a straightforward solution, I'll assume what I want to do is impossible, and add to the pile of occasionally useful solutions:

If you're always developing on the features branch, then you can merge features to master, and then, in master, git revert local. (Where local is the tag referencing the commit where you customized the paths, etc for your local environment.)

Now you must never merge master into features, because that would merge the reverse local commit too.

In this case master becomes sort of a deployment branch, only ever receiving merges from other branches. (Ideally, only from the features branch.)

This goes downhill very easily, just add another developer to the workflow and things get really messy. Still can be worked around by using explicit merge strategies, but it's generally a pain.



回答8:

I don't know if this would work, but:

  1. Create a commit that, given the "master" version of the config files, turns them into the version you need locally. Note the SHA-1. We'll call it MAKE_LOCAL
  2. Create a commit that, given your local version of the config files, turns them into the version appropriate for master. Note the SHA-1. We'll call it MAKE_REMOTE
  3. Using git hooks, when you commit:
    1. git cherry-pick MAKE_REMOTE (or use git diff and patch)
    2. Allow the commit to commence
    3. git cherry-pick MAKE_LOCAL (or use git diff and patch)

I think there is an even better way of transforming files in this manner, but I can't recall (if you can find shacon's git presentation from RubyConf, and can wade through 800 slides, it's in there with some great examples).



回答9:

The question is an old one, but I still have not found a good answer. Currently I am facing the same issue and below is my workaround to deal with it:

There are two branches in my local repo: master and local_settings. Having cut off the local_settings branch from master I committed there all local paths, not tagging and not trying to remember them. During local development I am switched to the local_settings branch, so I can run an application using local paths. But when it is time to commit I stash a current state and switch to the master branch. Then I pop the stashed changeset and commit it into master. And the final step is to switch back to local_settings, merge from master and continue development. To recap: I commit into the local_settings branch only changes that will stay locally and will never go into master; and no merges from local_settings to master.

Now let's say I need to add a "good" modification to a file with a local path added earlier, but the "good" modification is wanted in the master branch. I do my changes when the working copy is a head for local_settings, stash it and check out master. The stash keeps a changeset, that is relative to local_settings, although I am on master already. git stash pop applies the stashed changeset to the working copy and ends up having a diff relative to master, but only with the recent modification excluding the local path that had been added earlier and was not a part of the recent stashed changeset. Hence it can be committed without messing paths in the master branch. Afterwards again merge from master to local_settings.