A couple of sometimes popular tricks when working with git

  • Tutorial

I want to share recipes for solving a couple of tasks that sometimes arise when working with git, and which are not "directly obvious."

At first I thought to accumulate more similar recipes, but there is a time for everything. I think if there is a benefit, then it is possible and gradually ...


Merging old branches with minimal pain

Preamble. There is a main branch ( master) in which new features and fixes are actively committed; there is a parallel branch feature, in which the developers swam for some time into their own nirvana, and then they suddenly discovered that they hadn’t been merged with the master for a month, and the merge head-on (head with head) became nontrivial.

(Yes, this is not about an ideal world, where everything is correct, there is no crime, children are always obedient and even cross the road strictly by the hand with the mother, carefully looking around).

Purpose: to bear. In this case, that it was a "clean" merg, without features. Those. so that in a public repository in the graph of branches, two threads are connected at a single point with the message "merged branch 'master' into feature". And the whole “this one here” headache about how much time and effort it took, how many conflicts it was decided and how much hair it turned gray has not been kept.

The plot. The fact that in the gita you can edit the last commit with a key --amendeveryone knows. The point is that this "last commit" can be anywhere and contain anything. For example, it may not just be a “last commit to a linear branch”, where they forgot to correct a typo, but also a merge commit from a normal or “octopus” merge. --amendin exactly the same way, it will roll the proposed changes and "inline" the changed commit into the tree, as if it really appeared as a result of honest merging and conflict resolution. In fact git merge, git commit --amendit allows you to completely separate the “stalled place” (“this commit will be HERE in the tree”) and the content of the commit itself.

The basic idea of ​​a complex merge commit with a clean history is simple: first, "we pile up the place", creating a clean merge commit (regardless of the contents), then rewrite it with help --amend, making the contents "correct."

  1. "Column place." This is easy to do by designating a strategy during the merzh that will not ask unnecessary questions about conflict resolution.

    git checkout feature
    git merge master -s ours

  2. Oh yes. It was necessary to create a "backup" branch from the head by feature before merge. After all, nothing is really merged ... But let it be the 2nd point, not 0th. In general, we are switching to a non-merged feature, and now we honestly merge. Any affordable way, regardless of any "dirty khaki." My personal way is to look through the master branch from the moment of the last merge and evaluate possible problem commits (for example: they fixed a typo in one place - not a problem. Massively (into many files) they renamed some essence - a problem, etc.). From problematic commits we create new branches (I make artlessly - master1, master2, master3, etc.). And then we merge a branch for a branch, moving from old to fresh and correcting conflicts (which are usually self-evident with this approach). Other methods are suggested (I am not a magician; I only learn; I will be glad to constructive comments!). In the end, having spent (maybe) several hours on purely routine operations (which can be entrusted to a junior, because there are simply no complicated conflicts with this approach), we get the final code state: all innovations / fixes of the wizard are successfully ported to the feature branch, all relevant tests this code went through, etc. Successful code must be commited.

  3. Rewriting the "success story". Being on commit, where "everything is done", run the following:

git tag mp
git checkout mp
git reset feature
git checkout feature
git tag -d mp

(I decipher: with the help of the tag (mp - merge point) we go to the detached HEAD state, from there reset to the head of our branch, where at the very beginning there is a "fixed place" with a fraudulent merge commit. The tag is no longer needed, therefore we delete it). Now we are standing on the original "clean" merge commit; at the same time in the working copy we have the "correct" files, where everything necessary is merged. Now you need to add all the modified files to the index, and especially carefully look at non-staged (there will be all the new files that have arisen in the main branch). All the necessary from there add too.

Finally, when everything is ready, we enter our correct commit into the reserved place:

git commit --amend

Hooray! Everything worked out! You can casually push a branch into a public repository, and no one will know that you actually spent half a day of working time on this merge.

Upd: More concise way

Three months after this publication, the article “ How and why to steal trees in git ” by capslocky

For her reasons, you can achieve the same goal in a shorter way and without supporting mechanisms: you don’t need to "stumble a place", consider unstaged files after reset and make a amendment; You can in one step create a direct merge commit with the desired content.

We start immediately with the merger using any available methods (as in paragraph 2 above). A weighty intermediate history and khaki still do not matter. And then, instead of paragraph 3, with the substitution of a merge commit, we are doing an artificial merge , as in the article:

git tag mp
git checkout feature
git merge --ff $(git commit-tree mp^{tree} -m "merged branch 'master' into 'feature'" -p feature -p master)
git tag -d mp

All the magic here is done in one step by the third command (git commit-tree).

Select a part of the file, keeping the history

Preamble: in the file the code-code, and finally the code so that even the visual studio began to slow down, digesting it (not to mention JetBrains). (Yes, we are again in the "imperfect" world. As always).

Smart brains thought, thought, and identified several entities that can be budded into a separate file. But! If you just take a copy of a piece of a file and paste it into another one, this will be a completely new file from the point of view of git. In case of any problems, a search in history will unequivocally indicate only "where is this invalid?" Who divided the file. And finding an original source is sometimes not necessary “for repression”, but purely constructive - to find out WHY this line was changed; what bug it fixed (or fixed any). I want the file to be new, but the whole history of changes still remains!

The plot. With some slightly annoying edge effects, this can be done. For definiteness, there is a file file.txtfrom which you would like to select a part in file2.txt. (and at the same time save the story, yes). Launch this snippet:

f=file.txt; f1=file1.txt; f2=file2.txt
cp $f $f2
git add $f2
git mv $f $f1
git commit -m"split $f step 1, converted to $f1 and $f2"

As a result, we get the files file1.txtand file2.txt. They both have exactly the same story (real; like the source file). Yes, the original file.txthad to be renamed; this is the "slightly annoying" edge effect. Unfortunately, I could not find a way to save the history, but in order not to rename the source file, I could not (if anyone could, tell me!). However, git can handle it all; no one bothers now to rename the file back with a separate commit:

git mv $f1 $f
git commit -m"split finish, rename $f1 to $f"

Now, file2.txtgilt will show the same line history as the original file. The main thing is not to merge these two commits together (otherwise all the magic will disappear; I tried!). But while no one bothers to edit files directly in the process of separation; it is not necessary to do this later with separate commits. And yes, you can select many files at once!

The key point of the recipe: rename the source file to another in the same commit where a copy (s) is made from it (and, possibly, edited). And let this commit live in the future (never get rid of the reverse renaming).

Upd: a couple of recipes from Lissov

Separate part of the repository with history

You are on the latest version of the initial repository. The task is to separate one folder. (I’ve seen options for several folders, but it’s simpler and clearer to either put everything in one at first, or repeat what’s written below several times.)

Important! All moves to make a team git mv, otherwise git could lose history.

We carry out:

git filter-branch --prune-empty --subdirectory-filter "{directory}" [branch]

{directory} is the folder to be separated. As a result, we get a folder along with the full history of commits only into it, that is, in each commit, only files from this folder are displayed. Naturally, part of the commits will be empty, removed by --prune-empty.
Now we change origin:

git remote set-url origin {another_repository_url}`
git checkout move_from_Repo_1

If the second repository is clean, you can immediately in master. Well, push:

git push -u move_from_Repo_1

The entire snippet (for easy copy-paste):

directory="directory_to_extract"; newurl="another_repository_url"
git filter-branch --prune-empty --subdirectory-filter "$directory"
git remote set-url origin "$newurl"
git checkout move_from_Repo_1
git push -u move_from_Repo_1

Merge two repositories together

Suppose you have done what is higher than 2 times and received brunches move_from_Repo_1and move_from_Repo_2, and in each transferred files using git mvwhere they should be after the merge. Now it is necessary to bear:

br1="move_from_Repo_1"; br2="move_from_Repo_2"
git checkout master
git merge origin/$br1 --allow-unrelated-histories
git merge origin/$br2 --allow-unrelated-histories
git push

The focus is on "--allow-unrelated-histories". As a result, we get one repository with a complete history of all changes.

Also popular now: