Working with Git
This document is a guide to working with the Git version control system, tailored to developers familiar with Subversion.
Basic setup
Git commits identify authors and committers by name and email address. You will likely use your name and a personal email address for most Git projects, so you can add them to $HOME/.gitconfig
:
$ git config --global user.name 'Foo Barbaz' $ git config --global user.email 'foo.bar.baz@email.com'
If you're a MacPorts committer, you should use your MacPorts email address while working in MacPorts repositories. You can override your global settings by modifying /path/to/MacPorts/repo/.git/config
:
$ cd /path/to/MacPorts/repo $ git config user.email foobarbaz@macports.org
To get proper attribution for your commits on GitHub, please add and verify this email address in the GitHub settings. Also check that the Trac preferences have the correct email address.
Common git
tasks
Cloning a repository
Obtaining a local copy of a Git repository is called "cloning" because you usually end up with a carbon copy of the source repository. GitHub allows cloning over HTTPS or SSH:
$ git clone https://github.com/macports/macports-ports.git
$ git clone git@github.com:macports/macports-ports.git
Both of these create a macports-ports
directory containing a working tree at the latest commit, with the repository itself—with full history—at macports-ports/.git
.
If you use this as your working local repository check 4.6. Local Portfile Repositories (especially step 5) in the documentation for instructions on how to handle a local repository
(A list of our repositories is available at FAQ/GitHubMigration#repositories.)
Committing changes in your working copy
A fundamental difference between Subversion and Git working copies is that svn commit
by default commits all changes in your working copy, but git commit
by default commits none. Git uses a staging area called "index" that allows you to mark changes for inclusion in the next commit. To add changes to the next commit, use
git add <filename>...
git status
gives you an overview of the current index and your working copy. Additionally, it lists the commands to revert local uncommitted modifications (git checkout -- <filename>
) and to remove files from the next commit, but preserve the modifications in your working copy (git reset HEAD <filename>
).
Once you have chosen which files to include in your next commit using git add
, it is a good practice to review this list using
git status
and show the diff to be committed using
git diff --cached
If you are not satisfied with your changes, you can keep changing your files. Note that you will have to add any new modifications to the index using git add
again. Once you are satisfied with your change run
git commit
which prompts you for the commit message. See CommitMessages for more information on git conventions and expectations in commit messages.
Because of Git's distributed nature, a commit on your local machine is not immediately available on the central server, like it was with Subversion. This means that you can continue to prepare further changes in additional commits before you publish your changes as a set. In fact, it is a very common practice in Git to do many small changes that are logically consistent in themselves and then publish them in one step.
If you have commit access, you can publish your commits using git push <remote-name> <branch-name>
. <remote-name>
is the name of the repository to which you want to push. The most common push target is the location you initially cloned, which is automatically named origin
. <branch-name>
is the name of the branch you want to push. The Git equivalent to Subversion's trunk
is called master
. It is considered best practice to always specify your push target and the branch you are pushing, since git's default is pushing all branches that have a remote equivalent when you run git push
, which might publish changes that you do not consider final yet (you can disable this behavior by changing the push.default
git-config setting to nothing
, see git-config(1)).
git push origin master
Note that the push will fail if the remote repository has new changes. Contrary to Subversion, it does not matter whether your changes conflict with the remote ones. If this happens, you must update your local working copy as described in the section on fetch the latest changes and re-try the push.
Fetching the latest changes
Git's equivalent to svn update
is a little more complicated due to Git's distributed nature. Most of the complexity is not visible if you do not have commits in your working copy that have not been pushed yet. If both the local and the remote repository have changes (git calls them "diverged"), you will run into one of Git's core principles: Every commit has (at least) one parent commit, i.e. the commit history forms a directed acyclic graph.
Background knowledge
A picture is worth a thousand words:
A --- B --- C ---- R1 ---- R2 ---- R3 <= origin/master \ +--- L1 ---- L2 <= master
A, B and C are commits that are both in your local and in the remote repository. R1-3 are commits that have been pushed into the remote repository "origin"'s master branch while you were working. L1 and L2 are commits you prepared locally on your master branch. Git offers two different ways to bring R1-3 into your local branch:
Merging
A merge commit, created by git merge
, is a commit that has multiple parents. If no conflict occurs, merge commits do not usually have a diff attached (i.e. they do not modify files). On conflict, merge commits contain the diff that resolves the conflict. In pictures:
A --- B --- C ---- R1 ---- R2 ---- R3 <= origin/master \ \ +--- L1 ---- L2 ------ M <= master
The new commit M is the merge commit and can be pushed back to origin. This preserves the information that work was done in parallel, but unfortunately tends to mess up the history graph. See the attached screenshot of a commit history that always merges. To avoid this, you can instead rebase your changes.
Rebasing
Rebasing commits rewrites their parent commit IDs and avoids the need for a merge commit. Running git rebase origin/master
will take all commits in your local working copy that are not yet pushed and attach them after the end of origin/master
, which yields this picture:
A --- B --- C ---- R1 ---- R2 ---- R3 <= origin/master \ L1' ---- L2' <= master
Note that L1 and L2 have been modified by this operation; their commit IDs changed because of that. This new state can be pushed back to origin without the need for a merge commit, and the history graph will stay linear. We recommend that all developers rebase their changes rather than merge when conflicts occur during pushing.
Putting the background knowledge into production
First, get all new commits from the remote repository using git fetch <remote-name>
, where <remote-name>
identifies the repository from which you want to fetch and defaults to "origin":
git fetch
Then, rebase your local changes (if any) on top of any new changes in the remote repository and fix any conflicts that occur:
git rebase origin/master
Because these two operations are very common, Git offers a shorthand for them:
git pull --rebase
Note: git rebase
requires that you do not have uncommitted modifications in your working copy. If you have modifications, you can temporarily save them using git stash
and restore them after the rebase using git stash pop
. Alternatively, if you are using git version 2.6 or newer you can use the --autostash
option with git pull --rebase
. With this, you would run git pull --rebase --autostash
and this as its name suggests automatically does the git stash
prior to the rebase, and git stash apply
afterwards. Note that the MacPorts git
port provides an up to date version that supports this.
Warning: git pull
without the --rebase
flag is a shorthand for git fetch && git merge origin/master
, which will automatically create a merge commit if it thinks that's necessary.
If you do not want to remember passing --rebase
to git pull
every time you run it, you can set a couple of git-config(1) options to make it the default:
- Setting
pull.rebase
totrue
will change the default to always rebase when callinggit pull
. Note that this will also flatten any local merge commits you might have committed on purpose withgit merge
, which might be undesirable when merging development branches for MacPorts base. Consider using thepreserve
setting, which avoids this. - Rebasing can be enabled on a per-branch basis using the
branch.<name>.rebase
setting, which accepts the same values aspull.rebase
. - You can make
branch.<name>.rebase true
the default for all branches that you clone by settingbranch.autoSetupRebase
toalways
. This allows you to change the setting back to a different value for specific branches but still keep the default to rebase. Note that this setting will not affect branches that you have already created.
Reverting changes
Subversion has two methods for reverting changes: svn revert
, which drops uncommitted local modifications and restores the committed state and svn merge -c -12345
to undo committed changes.
Due to Git's distributed nature, there are three stages that can be reverted:
- To drop uncommitted modifications, use
git checkout -- <filename>
. If you had already added the file to the index usinggit add
, you have to unstage it first usinggit reset HEAD <filename>
.git status
prints these commands, so you don't have to remember them. - To undo a change that has already been committed and pushed, use
git revert <commitID>
. This will create a new commit that applies the inverse diff. Note that you still have to push this commit to publish it. - To throw away all changes that you have locally committed but not yet pushed, use
git reset --hard origin/master
. You will lose all your uncommitted and committed modifications. If that is not what you want, Git provides a variety of tools that allow you to change commits that you have not pushed yet (and theoretically also commits that have already been pushed, which will prevent you from pushing any changes again). Since this is an advanced topic it will not be covered here. As a pointer for further research, look forgit commit --amend
to change the topmost commit andgit rebase --interactive
, the so-called "interactive rebase", to change older commits.
Using a branch for development
Commonly, you'd use a local branch for development if you want this work isolated, such as when you are making experimental changes or it may take some time complete the work.
The life cycle for a branch, say "foobar", might look like this:
git branch foobar # creates a local branch git checkout foobar
- lots of edits and testing
git commit -m "blah blah"
- more testing and corrections
git commit -a --amend -m "revised blah blah"
- throughout the process, you can 'git checkout master' / 'git checkout foobar' to switch in and out of the branch.
- if there are upstream changes that need to be reflected in your branch...
git branch --set-upstream-to=origin/master foobar git pull --rebase
- finally, work is done. Now merge the changes into your local master...
git checkout master git merge --ff-only foobar
- check that only your expected commits are about to be pushed
git log origin/master..master
- normal steps to push
git pull --rebase && git push origin master
- if we're done with the branch, nuke it
git branch -d foobar
Common git
tasks while working with ports
Checking out a working copy of the ports tree
To get a working copy of the MacPorts ports tree to start changing ports, clone a copy of the repository:
git clone git@github.com:macports/macports-ports.git # or git clone https://github.com/macports/macports-ports.git # if SSH does not work on your network
This will give you the entire history of the ports tree, with the latest version being checked out in the filesystem.
Note: If you intend to submit a pull request on GitHub to get your changes included in MacPorts, you may want to create a fork of the repository first. To do that, go to https://github.com/macports/macports-ports/ and click the fork button at the top right. Then, run the command above, but use <yourusername>/macports-ports.git
instead of macports/macports-ports.git
.
See the section on committing changes to find out how to get your changes into the repository.
Submitting a pull request
If you are working on a fork of the ports repository, you can submit a pull request to have your changes considered for inclusion in the official repository. Follow the section on committing and pushing changes to push your changes back to your fork (if you git clone
d your fork, git push origin
will push your changes back to your fork on GitHub). Then, go to your repository on GitHub and click the "New Pull Request" button. You will see a preview of the changes and a button to create a pull request. Modify the message for the pull request as you see fit and confirm the creation of the pull request.
Note: This process is new to MacPorts developers. Please bear with us while we find the approach to pull requests that works best for us.
Updating your fork
When submitting pull requests, we may ask you to rebase your changes on top of our current master branch. The easiest way to do this is adding the official repository as a second remote to your working copy and pulling from it. First, use git remote
to add a reference to the upstream ports tree. Note that the name "upstream" can be chosen at random.
git remote add upstream git@github.com:macports/macports-ports.git # or git remote add upstream https://github.com/macports/macports-ports.git # if SSH does not work on your network
Then, fetch the contents of the upstream repository:
git fetch upstream
Finally, follow the section on fetching the latest changes but replace "origin" with "upstream". Eventually, you will end up running the equivalent of
git rebase upstream/master
which will put your local changes on top of ours. Pushing to your fork will update the pull request.
Working with someone else's pull request through its ID
This section describes how to work on a PR without the need to clone or pull from the PR author's repository.
1. Check out the pull request locally
Check out - according to GitHub's help - the pull request #ID
to work on as a local branch:$BRANCHNAME
:
git fetch upstream pull/ID/head:$BRANCHNAME git checkout $BRANCHNAME
alternatively you may use
curl -sLS https://github.com/macports/macports-ports/pull/$ID.patch \ | git am
or a wrapper script such as hub
hub am https://github.com/macports/macports-ports/pull/$ID
Put the proposed changes on top of the current upstream/master:
# --rebase option only needed if branch.autosetuprebase not set accordingly git pull --rebase upstream master
2. Test changes and modify commits
Work on the changes if required and commit your modifications.
# add changes to the latest commit git add Portfile git commit --amend # in case multiple commits have to be edited use git rebase -i
3. Publish changes
You have two options where to publish your changes. Option a) is to update the PR with your changes and then merge it on the GitHub web interface. Option b) directly pushes the changes to the upstream master branch.
a) Update PR with your changes
For this you have to find the source GitHub repository and branch the PR was created from. You can usually identify this on the GitHub web interface:
PR_AUTHOR wants to merge 1 commit into macports:master from PR_AUTHOR:PR_BRANCH
Use this information to update the PR by pushing your changes, where $BRANCHNAME
is the name of the branch you are currently working on:
git push https://github.com/$PR_AUTHOR/macports-ports $BRANCHNAME:$PR_BRANCH
b) Close PR and merge changes to upstream/master
# append "Closes: #ID" to the commit messages so that GitHub's PR gets auto-closed git commit --amend git push upstream master
Common git
tasks while working with MacPorts base
Checking out a working copy of MacPorts base
The source code of MacPorts itself is no longer managed in the same repository as all ports. Contrary to Subversion, checking out a sub-directory of a repository is not possible with Git. In order to avoid that all port maintainers have to clone the complete history of MacPorts base as well, the Subversion repository has been split into multiple separate repositories. MacPorts base is now available using
git clone git@github.com:macports/macports-base.git # or git clone https://github.com/macports/macports-base.git # if SSH does not work on your network
See the section on repository splitting during the export to get an overview of where a path in the old Subversion history is now available in Git.
Merge a single change from master into a release branch
The equivalent to Subversion's svn merge -c <revision> .
is git cherry-pick
. Use git cherry-pick
to apply a single change from master to a release branch. To do this, look up the commit ID of the commit you want to pick:
git log # copy the commit ID
Switch to the target branch of the cherry pick:
git checkout release-2.5
Cherry-pick the commit. It is good practice to pass -x
to git cherry-pick
, which will automatically add a "Cherry picked from commit <commmitID>" line to the commit message of your cherry pick. You will have the option to modify the commit message, e.g. to describe why the backport was necessary.
git cherry-pick -x <commitID>
Finally, push the new commit using
git push origin <branchname>
Tools
There are many third-party tools that complement or replace the Git command-line client.
- Command-line clients
- Graphical clients
- git gui is an open-source, Tk-based client included with Git itself.
- GitHub Desktop is a free client designed specifically for use with GitHub.
- gitk is an open-source, Tk-based repository browser included with Git itself.
- GitX is an open-source client designed for the Mac. (Our
GitX
port is currently nonfunctional and the project has been inactive since 2009.) - SourceTree is a commercial client that also supports Mercurial.
- Tower is a commercial client.
Learning Resources
Attachments (1)
-
commit-history-with-excessive-merging.png (345.7 KB) - added by neverpanic (Clemens Lang) 8 years ago.
Attach picture that shows the result of excessive merging
Download all attachments as: .zip