Skip to content

Git FAQ

Falko Galperin edited this page Sep 27, 2024 · 11 revisions

General

What should my commit message be?

We have no convention for this in SEE yet. However, there are a few rules that should be followed nonetheless:

  • The commit message should be a meaningful description of the changes. (Not something like "fix bug", "some changes".)
  • If the commit relates to a particular issue, include the issue number as a hashtag, for example, #123.
    • If the commit completely resolves or fixes an issue, you should put a closing keyword in front of the issue number (e.g., fix #123). That way, the issue will automatically be closed once the commit is merged into master.
  • The first line of the commit message must not be longer than 72 characters, otherwise Git clients (including GitHub) won't display the full message.
    • If there are additional details you wish to include, enter them in the body of the commit message, that is, leave an empty line after the title, then add the rest of the message in the lines below. The commit body is subject to the same 72 character limit.
  • The commit message should be in the imperative. For example, it should be Implement better code windows instead of Implemented better code windows.

Example for a good commit message:

Fix GraphProviders freezing SEE (closes #738)

The freezing was caused by the graph loading and drawing taking place
on the main thread. To resolve this, these parts of the code have been
made asynchronous, meaning that the operations are spread across
multiple frames of the event loop, which allows the UI to keep running.

There are automatic changes by Unity (e.g., meta files). Which files should I commit?

  • .meta files should be committed.
  • Scenes (.unity files in Assets/Scenes) should generally only be committed if it is absolutely necessary, since merge conflicts are hard to resolve here. If you do have to commit them, make sure they do not contain rendered code cities! If you've installed the git hooks using ./generateHooks.{sh,bat} (see Setup), this will be checked automatically when committing scene files.
  • In general, files that should not be committed are already in the .gitignore. However, if you are unsure, just ask in the Mattermost #Entwicklung channel.

I lost a commit / I messed up during a rebase and my work is lost / how do I go back to an earlier state?

Assuming you've accidentally overwritten/deleted a branch or commit, and can't just use a combination of git log/branch/checkout to get back to an earlier state, there's still one tool that can help in some cases: git reflog.

If you execute this command, you will get a list of earlier states that your repo was in—"earlier" as in "earlier on disk", not "earlier in the repo's history". If you see a state that looks like it might have what you want, just note the commit hash on the left and do git checkout <COMMIT-HASH>.

Rebases

What exactly is a merge, what exactly is a rebase?

Both of these are used in situations where you have some remote commits (e.g., on a master branch) and some local commits, and want to integrate the two branches. A merge creates a special merge commit that combines remote changes into the local branch (advantage: easy to do, disadvantage: creates a merge commit each time you do this). A rebase takes the current commits, and re-applies them (aka, rebases them) on top of the remote commits (advantage: clean history, disadvantage: needs a force push). In addition to the points mentioned above, a rebase has one more advantage in that it can, as opposed to a merge commit, rewrite the history. This means that commits can be dropped, squashed, changed, and so on—in a merge, all changes would still be present in the history and could not be removed, which is bad if you committed something you didn't want to commit.

However, an important caveat to rebases is that (unless you've just rebased local, unpushed commits) they need a force push (git push --force-with-lease), which is a dangerous operation, as it forcefully overwrites the remote branch—you should only do this if 1) it is absolutely necessary, and 2) you are the only person working on this branch!

Read this article for some nice diagrams and a more detailed explanation: https://www.atlassian.com/git/tutorials/merging-vs-rebasing

I've pushed something I didn't want to / I need to change the commit history / I need to edit older commits.

To do any of these, you need to do an interactive rebase. This can be a bit daunting the first few times, but it's a very useful tool for use cases like these. Please refer to "How do I rebase a branch?" below, and then use the edit action described in there to modify any offending commit (determine which commits you want to change first).

How do I rebase a branch?

Caution

A rebase can mess up unpushed changes, so push your changes first. Even more important, a rebase followed by a force push can even mess up remote/pushed changes! Please only do a force push if you are 1) certain that your local state is right and complete, and 2) nobody else works on this branch right now. Additionally, if you do need to force push, never use git push --force to do it, only ever git push --force-with-lease (the latter command stops the rewrite if someone else pushed new changes to the branch)!

Warnings like the above probably don't help with the trepidation around rebases, but it is important to keep this in mind—only do a force push if you are sure you want to overwrite the remote branch. Additionally, if you only need to rewrite local (i.e., unpushed) commits, you can do a rebase without needing to force push at all.

By the way, to get a more detailed explanation around how to do rebases, there's a short introduction here and a more detailed one here.

Here's a step-by-step rundown of how to do an interactive rebase (i.e., to change the commit history). Please read all the steps first before starting the rebase if you haven't done one before.

  1. git pull to make sure you have all recent changes pulled in.
  2. git rebase --committer-date-is-author-date -i origin/master to actually start the rebase. An editor will open after this.
    • The first parameter ensures the commits retain their correct authoring date.
    • The -i makes this an interactive rebase, meaning that you control how the history is rewritten.
    • The origin/master is the target branch onto which you want to rebase. Usually, this is the remote master branch.
  3. In the interactive rebase console that opened in your editor, you'll see a list of commits, to the left of which is the word pick. You can replace pick with something else to change a commit in some way. It is important that you only edit the word to the left of commits, not the commit message or commit hash. I will go over the most common kinds of changes:
    • pick: Uses the commit as-is. It will not be changed. (Default)
    • reword: Change the commit message. Remember not to change it within this console yet, you will be asked for the new message later on.
    • edit: Changes the "contents" of a commit, and usually what you want to do if you need to edit a commit. This requires some further explanation on how it's actually done, we'll go into this in step 5.
    • squash / fixup: This "squashes" the commit into the one above it. This means that, if you write squash/fixup next to a commit, it will be removed, and its changes will be put into the commit above it, combining the two. The difference between squash and fixup is that the former will also ask you what the new commit message should be (showing you both original messages in the prompt), while fixup will just throw away the second commit's message.
  4. Save the file and exit. The rebase will start now.
  5. The commits will be re-applied one after another.
    • For reworded or squashed commits, you will be asked for a new commit message, after which the rebase will continue.
    • For edited commits, you will be dropped into the terminal with a message that you can now change the commit. To actually change it, run git reset --soft HEAD~1, and you will drop into a state where all changes are added, but not yet committed. Do your modifications now, i.e., edit the changes how you want. Once you are done, add and commit your changes like you'd usually do, entering a commit message for this newly edited commit. Then, do git rebase --continue to continue the rebase.
    • If you get merge conflicts, resolve them with git mergetool, then do git rebase --continue.
    • To abort an active rebase at any point, do git rebase --abort.
  6. When the rebase is done, git status should yield a message at the top like "Your branch and 'origin/your-branch' have diverged, and have X and Y different commits each", where X and Y are some numbers. This indicates that your history has diverged, i.e., has different commits than the remote, which is good—the rebase has worked!
  7. Finally, and this is important, push your commits using git push --force-with-lease. This will overwrite the remote's history, so as the warning above states, make sure this is fine. Do not do a normal git push, or you may instead create a weird amalgamation of the two diverging histories.

And that should be it! If you did something wrong and overwrote the remote history, losing some commit/changes, there may still be hope—refer to "I lost a commit" above.

Why am I getting so many merge conflicts in a rebase (as compared to a merge)?

The reason for this is that in a rebase, each commit is applied individually, which means that conflict resolutions applied for one commit might come up in future commits again if the same region of code has been changed once more. In a merge conflict, all of these changes would be combined and applied at once, so it does not suffer from the same problem.

To alleviate this, you can do git config --global rerere.enabled true followed by git config --global rerere.autoupdate true (remove the --global if you only want to do this for the current repo). rerere is a mechanism in git that keeps track of already-resolved conflicts and how you resolved them, and then re-applies them if it encounters them again.

LFS

What is LFS, how does it work?

Normal Git is not equipped to handle large files/directories (>10MB) very well. SEE has several large components like these, which would usually slow the Git repository down a lot. To solve this, Git LFS exists—it puts the large files into a separate LFS repository that's made to handle large files well, and then stores pointers to these large files in the normal Git repository. The Git LFS client then resolves these pointers to the actual files at checkout by downloading from the LFS repo.

In the case of SEE, this solves another big problem: We would like for SEE to be open-source (and hence, public), but due to the way Unity works, we also need to include external plugins in the repository. Some of these plugins are paid and should not be available publicly. Since most of the plugins are pretty large, we solve this by putting those external plugins in LFS as well, and then making the LFS repository private. As a result, our LFS repository lives at the University of Bremen's GitLab, for which you need a University account.

image

How do I add a large file / a directory / an external plugin to LFS?

For the sake of this example, let's say your path you want in LFS is Assets/Plugins/SomePlugin.

  1. Make sure your path isn't yet added to LFS by checking the .gitattributes file for it.
  2. Do git lfs track "Assets/Plugins/SomePlugin/**". Note that both the ** and the quotes are important.
  3. Verify that .gitattributes has been changed and contains the new directory. (Additional changes may have happened due to git lfs track, such as the files having been replaced with LFS pointer files.)
  4. git add both .gitattributes and Assets/Plugins/SomePlugin. NOTE: If these files were already committed before now (i.e., before they were added to LFS), you will need to use git add --renormalize instead of the normal git add. Then, commit your changes.

How do I remove a file from Git LFS?

We need to distinguish two cases here:

  • You want to move an element from Git LFS into the normal Git repository.
    • To do this, you can use git lfs untrack "Assets/Plugins/SomePlugin/**", where the path needs to be an exact path from .gitattributes. After that, you'll need to do git add --renormalize "Assets/Plugins/SomePlugin/**" to remove currently tracked files from LFS and replace pointer files with actual file contents. Also, don't forget to add the changed .gitattributes.
  • You want to completely remove an element that's currently tracked in LFS from the repository.
    • You should be able to do this just like you'd remove a normal Git file/directory: Just do git rm -r "Assets/Plugin/SomePlugin".

Which files should be added to LFS?

As a rule of thumb, the following should be added to LFS:

  • Parts of SEE that should not be available publicly (e.g., external paid plugins)
  • Files that are bigger than 10 MB
  • Additions that are, taken together, bigger than 50 MB
    • "Additions" here refer to the additions within a single pull request.

However, these are just rough guidelines and not exact rules, so exercise your own judgement.

The git diff is unhelpful, it doesn't show the file contents. How do I fix this?

When looking at a git diff (e.g., when resolving merge conflicts) of an LFS file, it may be the case that rather than displaying the contents of the file, the LFS metadata file is compared (starting with version https://git-lfs.github.com/spec/v1).

To fix this, run:

git config diff.lfs.textconv cat

Troubleshooting

Possible GitScripts errors / CI fails

If the CI fails, please take a look at its output. Especially in the case of a failing static check or a failing GitScripts check, there's usually a detailed error message explaining what went wrong. You can also run the GitScripts checks locally using CI=1 ./GitScripts/run_all.

Note that for some errors, you will need to rebase your branch. For example, if you accidentally committed files belonging in LFS (i.e., very big files, or copyrighted files), it's not merely enough to move them to LFS in a new commit, you will also need to rewrite the history to completely purge them from the public repository. Refer to "I need to change the commit history" above to change offending commits.

Possible LFS trouble

Unfortunately, LFS can sometimes be a bit brittle. LFS errors are usually mentioned in git status, but to go absolutely sure, you can run git lfs fsck—if this doesn't complain about anything, LFS works perfectly fine. Otherwise, some common errors may be smudge errors, "Encountered file(s) that should have been pointers, but weren't", etc.

There's a few strategies for solving these, listed in order of ease:

  • If there are "broken" files listed (e.g., ones that should be pointers but aren't), this means that LFS expected them to be pointer files, but in reality they are either the actual file or an invalid pointer. To fix this, try running git add --renormalize <files>, where <files> is a space-separated list of all affected files.
  • If this doesn't work, make sure your changes are saved somewhere first, because this hacky fix will reset your repository to the state of the remote repository. Once you've made a backup of your changes, follow these steps, then reapply your changes.
  • Finally, if all else fails, clone a fresh copy of SEE. It's annoying, but it works and is usually faster than trying ten weird hyper-specific Git commands in a row that all don't work.
    • If this still doesn't fix your issue, there's either a problem with the GitLab server, your access to the server, or, worst of all, a Git LFS error has been merged into the master branch. In any case, please ask for help on the Mattermost's #Entwicklung channel.
Clone this wiki locally