Notes on Using GIT

1 Git and Emacs

What I would really like to see is an extension of PCL-CVS to allow it to interface with Git! Magit is more or less sufficient, though a wee bit different in its use.

Ideally for any Emacs VCS interface I'd like a feature which will show me a list of files that have been modified on a branch, or on a collection of specified branches, or in a tree of branches rooted at some other branch, even if some or all of those branches have been merged back to the root branch (i.e. topic branches from a local changes branch). This should then allow instant viewing of the differences in those files, vs. their branch root, as well as their differences with any other version on any other branch.

This would most mirror the setup I'm used to, i.e. with all local changes simply existing in a CVS working directory where PCL-CVS will show them to me.

The tree of branches idea would allow each individual change set to be stored in a branch, but allow me to see all the changed files on a "main" branch (e.g. a release/local branch) at once.

1.1 Magit

Magit comes closest to being ideal for using Git in Emacs.

Magit works extremely well for creating proper per-logical-change change sets from a large collection of wide-ranging changes in the local working directory.

Diffs can be refined into multiple hunks by changing the number of lines of context, and diff hunks can be individually included (staged) in a commit.

You can easily1 test a partial commit in isolation from other unrelated changes too, using the stash feature.

  1. First stage changes (files, or hunks of diffs to files) for the commit (press s on a file or hunk). Then stash everything (press z) but with the --keep-index option (press x instead of z again). It may be a good idea to stash un-tracked files too, typing -u (or -a to also stash ignored files) before typing x.
  2. Test the commit in isolation, commit it when it is ready (just don't make any unrelated changes when fixing anything for the commit). Remember to stage these new changes. To commit the staged changes press c c, edit the commit message, press C-c C-c. Finally pop the stash (press A with the cursor on its line).

Note: even though the changes in the index, i.e. in the staging area, and eventually in the commit, are included in the stash, they will not be applied when the stash is popped again after the commit is completed.

2 Other UI tools for Git

  • tig is a handy curses-based interface, though not terribly feature-full
    • works in a bare repository
  • GitX is a Mac OS X application that's kinda cool
    • works in a bare repository
    • has an easy way to show all branches, and does so nicely
  • gitk is a tcl/Tk interface – not bad, but a bit clunky to use
    • works in a bare repository
    • the --all option, to show all branches, should maybe be the default?
  • qgit a Qt2 based interface
    • kinda cool, but no bare-repo support
  • git cola: Git-Cola: Python-based UI
    • kinda hokey

3 Git vs. CVS

Some good notes about the differences between CVS and Git can be found here:

http://stackoverflow.com/questions/802573/difference-between-git-and-cvs

4 Using Git with a third-party CVS repository with git-cvsimport(1)

N.B.: From git-cvsimport(1):

WARNING: git cvsimport uses cvsps version 2, which is considered deprecated; it does not work with cvsps version 3 and/or later. If you are performing a one-shot import of a CVS repository consider using cvs2git or cvs-fast-export.

Git can sometimes directly import a third party CVS repository, and if desired also export changes back to that same CVS repository.

http://stackoverflow.com/questions/584522/how-to-export-revision-history-from-mercurial-or-git-to-cvs

http://stackoverflow.com/questions/1392925/how-to-mirror-one-one-git-remote-to-another-with-push

Basicaly the idea is to use git-cvsimport(1) to "clone" the CVS repository into a Git repository, then use Git as normal, and finally (optionally) export changes to a CVS working directory that is set up so that commits can be done back to the original repository.

Ideally you will only clone from the first Git repository created by git-cvsimport(1) in order for git push --mirror offsite to be able to fully mirror all of the CVS branches.

Note: Don’t use git push --mirror into repositories that weren’t cloned with --mirror as well. XXX hmmm…. ???

4.1 Using git-cvsimport(1)

git cvsimport -v [-i|-C dir_to_create] [-k] [-u] -d $CVSROOT cvs_module_to_checkout
  • -i will import only, like git clone --bare (but I think it assumes an existing ./.git directory), or with -C will create a working directory with a checked out copy.
  • -k will kill keywords to prevent confusion, as with CVS's -kk
  • -u will translate underscores in tags back into the more desirable .

You may need to set CVSREADONLYFS=1 in the environment for local access to a read-only repository.

For local repository access the CVS modules file is ignored. You must use the full sub-directory name for the module within the CVS repository.

4.2 Cloning the first imported repository

Create a clone of the Git repository that was imported from CVS so that you can work on it:

cd /work/$USER
git clone ../cvs_module dir_to_create

5 Using Git within a CVS working directory (e.g. for a read-only CVS repository)

It's possible to put a git repository inside a CVS working directory to enjoy the benefits of Git.

This can be done in order to keep track of, and separate out, multiple local changes to a project using CVS when you may not be able to submit those changes to the upstream CVS repository as they are made, perhaps because you don't have CVS commit rights and must submit patches, or because some policy prevents immediate commit (the repository is temporarily locked for pre-release testing, etc.)

5.1 Prepare a local CVS working directory

check out the CVS project into a desired working directory

cvs -d $CVSROOT co -d ~/work/project project

5.2 set up Git and CVS to ignore each other's special files

  • add .git and .gitignore to ~/.cvsignore
  • add CVS to .gitignore
    • XXX or maybe not – some people keep these in Git so that they can then clone the Git repo and still use CVS in that new clone, which makes a lot of sense sometimes, e.g. for making patches or seeing old CVS history, etc., etc.

5.3 Create the Git repository from the current CVS files and set initial sync tag

git init && git add . && git commit -a -m "initial check-in"
git tag cvssync

5.4 modify Emacs VC to prefer Git over CVS

You'll want the Emacs VC tools to operate on the local Git repository, not on the CVS repository:

;; Move git backend in front of cvs backend
;; (Emacs manual, Chapter 32, esp. 32.1.8.2 Local Version Control)
(setq vc-handled-backends
   (append
    (list 'Git)
    (delq 'Git vc-handled-backends)))

(need some way to make this work per working directory, not globally)

5.5 create and switch to a Git branch for local changes based on master

git checkout -t master -b local-hacks

It is possible to create any number of local branches in git, try things out etc.

git commit -am "A new local hack!"

5.6 Catching up to CVS

When someone using CVS updates something in the CVS repository you can simply switch your local repository back to the 'master' branch (git stash; git checkout -b master), then run cvs update, and finally commit the update to the git master branch (git commit -a), then switch back to what you were doing (git checkout -b local-hacks; git stash pop). When ready you can also merge updates from the master branch into your local branch (git merge master or git rebase master as appropriate);

5.7 Committing back to CVS (e.g. if you're a project member)

First off merge local work to master:

git checkout master
git merge local-hacks

Then update from CVS to merge central work with local work in CVS:

cvs update -d -p
# merge any conflicts!
# (don't need to commit if there are no changes from CVS)
git commit -am "Sync with CVS"

Verify and review what was done locally

git log cvssync..
cvs diff
# ... all is OK?  Push to CVS:
cvs commit -m 'Push local-hacks blabbety blah'
# iff there are Id tags, they will have changed
git commit -am "Sync CVS Id tags"
git tag -f cvssync

6 Converting a CVS repository to Git with cvs2git(1)

cvs2git(1), part of the cvs2svn package, can do a one-time conversion of a local CVS repository to a Git repository:

cvs2git --username=$USER --blobfile=blob --dumpfile=dump --keep-cvsignore \
    --encoding=iso-8859-1 --fallback-encoding=ascii --use-cvs $CVSROOT/path/to/module
cat blob dump | git fast-import --stats
rm blob dump
git checkout
# possibly rename or link or symlink .cvsignore files to .gitignore
# and adapt (no default ignores in Git)

See also my ~/notes/cvs2git

N.B.: cvs2git does an infinitely better job representing vendor branches than cvsconvert!

https://github.com/mhagger/cvs2svn.git

7 Git vs. SCCS

You can use git-sccsimport(1) to convert a collection of SCCS files into a Git repository. See, for example, the pushdotfiles function in my ~/.shrc file for how I use it to do a rolling migration of my dotfiles, which I continue to maintain using SCCS, but wanted to publish as a Git repository.

https://github.com/robohack/git-sccsimport

https://github.com/robohack/dotfiles/blob/master/.shrc

https://github.com/robohack/dotfiles

8 Quick and dirty Git equivalent to CVS "vendor" branch usage for Git-based Projects

This is for projects which are already using Git and/or have published Git repositories for their project. (Later I'll explore dealing with projects which do not export their repository in any form that can be use by, or converted into, Git.)

Slight variations for these techniques apply to use of any foreign repository which Git can directly access – currently this is Subversion (SVN). See the git-svn(1) manual page.

WARNING: Use this method only if you are the sole person managing the project locally.

The following section implicitly uses the git rebase command to manage local changes on the locally created branch(es). The effect of this is that the branch(es) on which it is used cannot sensibly be published (shared, cloned from, or pushed). This is because git rebase rewrites the history of the branch, effectively deleting it and then re-creating it (with the set of commits) from the new head of the branch which the local branch is "tracking". The new commits have different SHA1 identifiers, and may include conflict resolutions. Anyone cloning this repository and trying to use the locally created branch(es) for any further development would suddenly find their own local changes would disappear the next time they fetched your changes.

(Is there some way to make a repository un-clone-able?)

8.1 Clone the other project's published repository

git clone git://github.example.com/projects/projectname

8.2 Create a local branch for your changes from the origin/master branch:

First we create a local branch called local which "tracks" a remote branch (in this case the one called master in the repository from which we cloned our local copy), and we set the default action on our new local branch to rebase by default instead of merge. I.e. when we do a git pull in the future our local branch will be rebased onto the head of the origin/master branch (instead of the origin/master branch being merged to the local branch).

git checkout -t -b local origin/master
git config branch.local.rebase true

master is the default name of the trunk of the project revision tree. Any branch in the remote project can be checked out to be worked upon, for example a release branch. If you work on a non-trunk branch you might want to choose a local branch naming convention that reflects which origin branch it is rooted on (i.e. tracking), e.g., by pre-pending local- to the original branch name, like this:

git checkout -t -b local-v1.0 origin/v1.0
git config branch.local-v1.0.rebase true        # "-v1.0"???

Not all projects use release branches though, some just use tags, in which case if you set the starting point of your local branch to a release tag then the that will not ever be a moving target (assuming the upstream tag never moves) and so the use of "rebase" operations to track the remote release will not be necessary.

8.2.1 working entirely without a local branch

Note that if your repository really is effectively a "leaf node" (neither you nor anyone else ever clones from it and you never push it anywhere) then it is possible to work directly from the origin branch of your choice and just use git rebase to move your changes forward on that branch as new changes are fetched from the remote repository.

However by not using a local branch for local changes you forgo some of the advantages Git branch management tools and techniques can bring to the development process. The use of branches in managing local changes may be even more important when the remote project uses branches to manage different lines of development for releases, or major product variants, etc. – i.e. when you might (eventually) be interested in more than one branch in the remote project.

8.2.2 see also git config branch.autoSetupRebase always

This way the "rebase" action should always automatically be set to true for all new locally created "tracking" branches.

This could be handy if you always want the work on your local branches to be at the head of the branch it was made from, e.g., to always add portability patches, fixes, etc.; or if you want your changes to be cleanly cut from the most recent remote work so that they can most easily be submitted upstream as patches.

8.3 Make your changes to the project files and create a new commit

You would normally work on your local branch; there's no need to switch back to the master branch.

git add Makefile
git commit -m 'My local Makefile configurations'

You use git add filename because you are adding new content to be tracked. This is also called "staging". The current content of the modified file(s) is added to the "INDEX".

You can delay git commit until you've added (staged) multiple related content changes. You do not specify filenames to the commit command.

git commit only commits added (staged) changes by default, not all modifications to the working tree. However git commit -a will commit all modifications in the current tree (by implicitly doing the "add" operation first, literally as if git add -u were run).

8.3.1 be careful with committing when there are unstaged changes!

Staged and unstaged changes must always be completely independent of each other before you do a commit, i.e. where some changes are left in the working directory and will not be committed, since each commit must be a self-contained independent unit of change.

8.4 Update the local git repository with new changes from the origin

With your local branch configured to use "rebase" (instead of the default "merge" action) now each time you pull new commits from the distribution repository your local commits will be rewound and stashed temporarily, the local branch point will be moved to the new head of the origin/master branch, and the stashed changes from your local branch will be replayed on top of the new branch point.

git fetch           # \
                    # -> fetch + {merge,rebase} == "git pull"
git rebase          # /

Note that if you do the git fetch first then you can look at the changes which will be pulled:

git diff master origin/master
# or just (assuming you are on the master branch already)
git diff origin/master
# or alternately:
git diff ...origin

8.4.1 Use git stash to save un-committed changes when doing git pull

When you do a git pull your working directory must be "clean". To achieve this without committing your unfinished changes you can create a "stash" of your changes using git stash, and then you can "pop" the stash to replay it onto the new HEAD.

Another way to do this from the command line is with GitUp, specifically the Python version PyGitUp (see the section about git up)

git up

8.4.2 dealing with conflicts during the rebase:

If you get a conflict during a git pull you will need to edit the file with the conflict to fix up the conflicting lines and then tell git that you have resolved the conflict.

The conflict resolution process goes something like this:

  1. the git pull fails with a conflict
  2. edit the file(s) containing the conflict markers
  3. git add the file to mark the conflict resolved
  4. run git rebase --continue to resume the rebase
  5. lather, rinse, and repeat steps 2-4 as needed
8.4.2.1 So, why is there no commit of conflict resolution changes?

it seems that even if the result of resolving a conflict with a local change is not identical to the original local change, no new "commit" is necessary, thus the log message for the replay of the now-different local change may be off, or even completely wrong.

i.e. it seems as if the result of merging a conflict is treated exactly as if it were the original local change – all meta information about the original local change is re-used when the result is committed again to the local branch.

This makes sense if the result of the conflict resolution has the exact same intent as the original local change. However if the result of the resolution is somehow a modification of the original intent then the original commit message may not exactly match the new intent of the change

So, therefore the result of performing a conflict resolution should always exactly match the intent of the original local change. If the conflict creates a situation such that the original local change must now be modified in intent then the rebase action should be to skip this local change, and then after the rebase is done a new local change should be made to implement the new modified intent.

  1. run git rebase --skip for this conflict, then repeat steps 2-4 above again, or this step, as necessary for any further conflicts
  2. finally re-implement the skipped change(s) and add and commit them to the local branch.

8.5 Moving local changes from one branch to another: Branch Migration

You may need to move some/all of your local changes to another branch in the remote project, either to upgrade/downgrade your local changes to another release branch, or perhaps to move your changes to the trunk so that they can be submitted upstream.

Basically this is done by copying the changes from one branch (your local branch, for example) to another, while of course leaving them intact and unchanged on the original branch.

8.5.1 Using git cherry and git cherry-pick to move changes

Another way to do this, which retains the same logical set of changes and the order in which they were made, is to use git cherry to create a patch, or set of patches, based on all of the commits on the local branch and then apply the patch(es) to a new local branch which is rooted on the new release branch.

git checkout -b local-v2.0 v2.0
git cherry origin/master local | awk '$1 = "+" {print $2}' | xargs -p -n 1 git cherry-pick -x

You'll need two windows to do this if there are any conflicts. Do not proceed to the next git cherry-pick if there is a conflict, refer to the next sub-section before continuing.

For the moment cherry picking is probably more easily done with a better user interface, such as "gitk" or Emacs Magit, etc.

8.5.1.1 handling conflicts during cherry-pick branch migration

We use xargs -p -n 1 above to be sure that only one git cherry-pick command is run at a time. Press y<CR> to run each command only if the previous one completed successfully.

If a conflict is detected when git cherry-pick attempts to apply the change to the new branch the conflict must be resolved and a git add followed by git commit must be run as instructed before the next git cherry-pick is run.

8.5.2 Using git log -p and git am to move changes

(This is effectively what git rebase does under the hood, except there the changes are moved, not copied.(?))

Yet another way to copy changes from a branch, which also retains the same logical set of changes in the order in which they were made, is to use git format-patch to create a patch, or set of patches, based on all of the commits on the local branch and then apply the patch(es) to a new local branch which is rooted on the new release branch using git am.

This is the same process a project contributor and a project manager might use move patches from the contributor to the project, except of course that there the patches applied to the project's master repository, not back into the same repository.

git checkout -b local-v2.0 v2.0
git log -p  v1.0..local-v1.0 | git am -p

8.5.3 Using Stacked-Git to manage patch sets to move changes

There is also a Git add-on called Stacked Git which can be used to manage patch sets which can then be applied to each supported release branch as desired.

Copying changes using the previous methods creates separate, unique, commits on each branch, even if the changes (and their commit messages) are identical (though with git cherry-pick -x the message will actually be different).

[ … ToDo: write up how Stacked Git avoids this issue … ]

it creates only one single commit in the repository for each change required as part of the feature/fix?

9 Doing the GitHub Dance with private changes on a branch in a GitHub fork

managing private branch in a github fork:

See also GitHub: Configuring a remote for a fork

https://help.github.com/articles/configuring-a-remote-for-a-fork/

N.B.: First also see below about the "hub" command-line tool and how it can do GitHub forking from your local command-line. The GitHub document above recommends using the remote name "origin" for the fork, and "upstream" for the original repository. However the hub tool starts with a clone of the original, where the remote name will be "origin", and then after using hub fork you still have the same remote name "origin" pointing at the original repository and (assuming you didn't supply a --remote-name= option) a new remote pointing at your fork (with "git@github.com:USERNAME" syntax) with your GitHub username as the remote name.

On the other hand lots of blogs and how-tos, including the https://hub.github.com/ examples, suggest using the remote name "origin" as if you had initially cloned your fork, and then adding the generic remote name "upstream" to point to the original repository.

However these generic names can get confusing, "upstream" has several possible meanings, and "origin" would seem to suggest "original", so using the default hub fork setup seems to be best, and that's what I'll continue to do. (Note git remote rename might help mixups.)

If you don't use hub fork then fork on GitHub: login to GitHub, navigate to the owner's repo, and push the "Fork" button.

Since you may already have changes, possibly branches and commits, in an existing repository cloned from github, you can re-use it with a github fork.

So first clone the original repo, if that has not been done already:

git clone https://github.com:original/repo /work/woods/m-repo

Now if you didn't use hub fork then you must add your new fork as a new remote (and rename the directory to indicate it's now a forked repo):

cd /work/woods/
mv [mg]-repo f-repo             # for originals with old name...
cd f-repo
git remote add robohack git@github.com:robohack/repo.git
git fetch --all

Push any existing local branch(es) to your forked repo:

git push -b local robohack/local

configure the repo to normally always rebase (except for master)

git config branch.autoSetupRebase always

change the "master" branch to not use rebase, and merge –ff-only (not (yet) doable in magit)

git config branch.master.rebase false
git config branch.master.mergeOptione --ff-only

set (all) [local] branches to have a pushRemote = robohack (I used to do this individually, but I think this global option works better/safer.)

git config remote.pushDefault robohack

create and checkout a local "local" changes branch (if not already done)

(in magit: b c \[return] l o c a l \[return])

git branch -b local origin/master

Do some `forge' stuff:

  • also fetch any pull requests (the github way) (so you can do: "git checkout -b pr/42 pull/origin/42")
git config --local --add remote.origin.fetch '+refs/pull/*/head:refs/pull/origin/*'
  • also fetch any pull requests (the magit/forge way) (see also `forge-add-pullreq-refspec' emacs)
git config --local --add remote.origin.fetch '+refs/pull/*/head:refs/pullreqs/*'

9.0.1 An example result

The .git/config file should look a bit like this:

[core]
	repositoryformatversion = 0
	filemode = true
	bare = false
	logallrefupdates = true
[branch]
	autoSetupRebase = always
[pull]
	rebase = true
[remote]
	pushDefault = robohack
[magit]
	gh-pulls-repo = wanderlust/apel
[remote "origin"]
	url = https://github.com/wanderlust/apel
	fetch = +refs/heads/*:refs/remotes/origin/*
	# also fetch any pull requests (the github way)
	fetch = +refs/pull/*/head:refs/pull/origin/*
	# also fetch any pull requests (the magit/forge way)
	fetch = +refs/pull/*/head:refs/pullreqs/*
[branch "apel-wl"]
	remote = origin
	merge = refs/heads/apel-wl
        mergeOptions = --ff-only
	rebase = false
[remote "robohack"]
	url = git@github.com:robohack/apel.git
	fetch = +refs/heads/*:refs/remotes/robohack/*
	# also fetch any pull requests (both ways)
	fetch = +refs/pull/*/head:refs/pull/robohack/*
	fetch = +refs/pull/*/head:refs/pullreqs/*
[branch "local"]
        description = "Local integration branch."
	remote = robohack
	merge = refs/heads/local
	rebase = true

9.0.2 Command line tools for forking on github

GitHub's own "hub" tool, written in Go:

git clone https://github.com/github/hub f-hub
cd f-hub
hub fork

This tool uses your github user name as the useful remote name, while leaving the original project with the remote name "origin". Perhaps this makes more sense than using the confusing "upstream".

9.1 process for managing merges from upstream:

see:

https://help.github.com/articles/syncing-a-fork/

[ … ToDo: write this up properly in detail … ]

magit-status
create a stash (z z) [or otherwise kill unstaged changes -- e.g. reset hard (X h)]
Fetch all (f a) [git fetch --all] or better yet [git up], or even [git clone --mirror]
checkout (the local remote-tracking) master (b b) [git checkout master]
merge from upstream (F u) [git merge -ff-only upstream/master]

[ … there's also a theory that you might want a merge commit, so "-no-ff" … ]

update master on robohack, i.e. push to github fork (P p) [git push robohack master]
checkout your local branch (b b local) [git checkout local]
merge from master (m m) [git merge master]

[ … or should that normally always be (r e) [git rebase master local] ??? … ]

[[ fix, commit, etc. -- make sure local changes are committed! ]]
update 'local' on robohack, i.e. push to github fork (P p) [git push robohak local]

N.B.: Note that some(most?) upstream projects would rather the local changes be rebased onto the tip of their current development.

N.B.: be very careful not to "pull upstream" in the local branch! (only merge) [ … or "git rebase master local" ??? … ]

N.B.: leave the "Unmerged into origin/master" changes alone as they will have to go through upstream/master via a pull request to ever appear in origin/master

9.2 GitHub "Pages" for forks

THIS IS PROBABLY NOT A GREAT IDEA!!! You cannot easily maintain local changes on an evolving branch, even if you rebase your changes to the HEAD after every pull from the upstream branch. Your local changes will forever appear to need pushing to the upstream even though you will not be permitted to do that.

Some (many?) GitHub public repositories have their own web "home" page at https://author.github.io/project.

However the default "fork" action on GitHub does not copy the "gh-pages" branch, and thus the fork does not automatically get its own web page based on the origin.

To copy this branch you can (after stashing any uncommitted changes):

git checkout gh-pages upstream/gh-pages
git push gh-pages origin

Then go to your forked repository on github and configure it under the "Settings" tab at the "GitHub Pages" section by selectin the source as the "gh-pages branch".

You will then probably have to make a change to one of the files on that branch and push it in order to get GitHub to actually create and publish the web page(s).

10 The Git equivalent to CVS "vendor" branch usage for non-Git compatible Projects

Git can also be used to manage local changes to projects which do not publish their public Git repositories in any way compatible with Git. This is done this is done in a manner which is almost identical to how one does it with CVS Vendor branches, but with Git it is much simpler overall.

This technique uses a branch called vendor on which new vendor releases are "imported" (added and committed, from their official distributions). All local changes will be done on a branch called local.

Note that while Git can use heuristics to determine when identical content moves between files, it is rarely the case that content moves and but does not also change between vendor releases.

FYI: The DragonFly BSD folks, who are already using Git, have established similar procedures outlined in their devlopment(7) manual page. See the "VENDOR IMPORTS" section here:

http://leaf.dragonflybsd.org/cgi/web-man?command=development&section=ANY

10.1 Initial creation of a project repository with a vendor branch

mkdir fooproj
cd fooproj
git init
echo 'ref: refs/heads/local' > .git/HEAD
pax -rz -s '/^fooproj-1.0//' -f ~/Downloads/fooproj-1.0.tgz
git add .
git commit --author='VenDor' -m 'Import of fooproj version 1.0'
git checkout -b vendor
git tag fooproj-1.0
git checkout local

10.1.1 optionally push the local and vendor branches to the central server

git push $GITSERVER/fooproj local vendor

10.2 Making local changes to a vendor-originated project

10.2.1 start by cloning the fooproj repository

cd ~/work.d
git clone $GITSERVER/fooproj

10.2.2 next create a branch following our "local" branch for your new modification

git checkout -b new_fix local
# edit, test, "git commit -a", etc.

10.2.3 finally merge the final changes for your new modification to our "local" branch

git checkout local
git merge new_fix --no-ff
# fix any conflicts, if necessary, and commit those fixes

10.2.4 optionally push the local branch back to the central server

git push $GITSERVER/fooproj local

10.3 Importing a new release to an existing vendor branch

10.3.1 start with importing the new vendor code to the vendor branch

First we switch our working directory to the vendor branch, then we clean it out entirely, and next we extract the new vendor release in place. We now have the working directory in the state of the new vendor release and all we have to do is tell Git about this new state. First we can add everything, recursively. Next we have to look for, and account for, any files which no longer exist in the new vendor release. Finally we can commit and tag the changes to update the vendor branch in our Git repository to reflect the state of the new vendor release.

cd fooproj	# or "git clone $GITSERVER/fooproj && cd fooproj"
git checkout vendor
rm -rf $(find . -maxdepth 1 ! -name .git ! -name .)
pax -rz -s '/^fooproj-2.0//' -f ~/Downloads/fooproj-2.0.tgz
git add .
git status
git rm $(git status | awk '$2 == "deleted:" {print $3}')
git commit -m 'Import of fooproj version 2.0'
git tag fooproj-2.0

10.3.2 next merge the new vendor release to our "local" branch

git checkout local
git merge vendor --no-ff
# fix any conflicts, if necessary, and commit those fixes

10.3.3 optionally push the local and vendor branches to the central server

git push $GITSERVER local vendor

11 Migrating From one Git Server (or Local Original Repository) to Another

Basically the idea is simply to create an empty repository on the new server and then mirror your current repository to it:

git clone --mirror ssh://git@bitbucket.org/robohack//example-repo
cd example-repo

Optionally go to https://github.com/new and create the new repository. Do not tick (or un-tick) any of the suggested README or LICENSE auto-generation options.

hub create robohack/example-repo

Then either (if converting from another Git server):

git remote add new-origin git@github.com:robohack/example-repo.git
git push --mirror new-origin

OR (if starting from a local original):

#XXX not necessary XXX: git remote add origin git@github.com:robohack/example-repo.git

# also fetch any pull requests (the github way)
# (so you can do:  "git checkout -b pr/42 pull/origin/42")
git config --local --add remote.origin.fetch '+refs/pull/*/head:refs/pull/origin/*'
# also fetch any pull requests (the magit/forge way)
# (see also `forge-add-pullreq-refspec' emacs)
git config --local --add remote.origin.fetch '+refs/pull/*/head:refs/pullreqs/*'
git config branch.autoSetupRebase always
git config branch.master.merge refs/heads/master
git config branch.master.remote origin
git config remote.pushDefault origin
git push --mirror

If (converting from another Git server and) you're well good and done with the original server you can just change the URL for the "origin" remote:

git remote set-url --push origin

With the first or last forms you can continually migrate without having to re-clone:

cd example-repo
git fetch --all -p
git push --mirror new-origin

Then there's the option of setting git pull options:

git config pull.rebase false
git config branch.master.rebase false

See also:

https://gitenterprise.me/2016/03/30/how-to-migrate-a-git-repository/

12 Process for merging pull requests with "Magit Forge"

Magit Forge

  1. Check out the pull-request using b y. That goes beyond just creating a local branch. It also creates a remote if necessary, configures upstream and push-target, etc.
  2. Review, possibly making changes.
  3. If you made changes, then push them. This works even if the feature branch lives on someone else's fork. (Unless they did not bother with a feature branch and ask you to merge to their master, which you shouldn't.)
  4. Merge locally and then push to the "pull-request". Almost always that means merging into "master" and then pushing to "origin/master". A good way of doing that is to use the "merge into" command on m i, that automatically suggests the appropriate branch.

When you merge a branch corresponding to a pull-request, then Magit automatically cleans up after you (asking some questions for safety reasons). So normally after you have merged (or deleted) a pull-request branch, the branch and its remote are removed and everything is nice and tidy.

13 Adding additional sub-modules from other Git repositories

Other sub-projects, third-party or local, can be included in a larger project by using Git's "submodule" support.

cd superproject
git submodule add -b local git://hostname/subproj.git subdir/subproj
git submodule init

The branch used in the super-project (here called local) should be one that can be used to track integration changes to the sub-project. This branch in the sub-project can then be rebased onto any given release branch of the sub-project so that integration changes can be migrated from one release of the sub-project to the next.

The reference URL for the sub-project must be a stable central location for the sub-project, probably on a local server as these integration changes will likely have to be pushed to it so they can be shared with the group.

The final parameter is the relative path from the root of the superproject where the sub-project should be cloned to and checked out.

Changes can be made in the subdir/subproj working directory, but they must be committed to the sub-project and pushed to the reference repository BEFORE any commit can be done in the superproject. This is because the super-project references a specific commit in the sub-project, and thus git submodule update can wipe out any un-pushed changes.

Cloned copies of the superproject also need special attention. If you clone a Git repository and find it contains a ./.gitmodules file then you need to set up these sub-modules:

git submodule init
git submodule update

This needs to be repeated if the ./.gitmodules file changes too.(?)

14 Git Development of New Features/Fixes with Topic Branches

WARNING: this methodology may need to be re-thought in conjunction with the idea that Git merges things based on its whole directed-graph structure, not based on branch base points

See also: gitworkflows(7) manual page!

See also: Git User Manual, Section 5, sub-section: "Keeping a patch series up to date using git rebase"

https://git-scm.com/docs/user-manual.html#using-git-rebase

See also: The git flow extensions. Majorly cool!

14.1 Using topic/feature branches to create change sets for local changes

Ideally, each self-contained feature is developed on its own appropriately named branch. When it is reasonably stable, it can be merged into one of the long-lived branches.

Creating a "topic" or "feature" branch, then merging that branch to each of the other branches which require the same changes (e.g., to each supported release) is the ideal way to manage more complex local changes, especially ones which are intended to be pushed upstream for eventual inclusion in the origin/master branch by upstream project managers.

Some folks suggest always using the "Branch Early and Often" methodology with Git, i.e. avoid doing any development ever on the master (trunk) branch – just do merges there. A branch in Git is in some ways the moral equivalent of a patch. One can periodically create a testing branch onto which all development ("topic" or "feature") branches are merged to see if everything works together; deleting the testing branch when done with each iteration of QA testing; and then merging all of the tested and verified "topic" or "feature" development branches to the master branch.

"Feature" branches should be forked from the oldest locally supported release branch (not the local branch forked from the original release branch, but rather just the original release branch itself), since the feature/fix they represent should not initially depend upon newer changes in newer releases (or in the trunk) as otherwise supporting the "topic" branch on the older releases may become too difficult.

Naming these branches with a feature/ prefix can help distinguish them from other long-lived branch names.

For features intended only for new, as-yet un-branched, releases it can be common that these branches will be rebased onto the HEAD of the current development or master branch frequently. In order to distinguish these branches from long-lived branches they can be marked as being "works-in-progress", e.g., wip/shiny-feature. After a rebase such branches will of course have to be pushed with git push --force-with-lease wip/shiny-feature. A wip/ prefix on your branch name will indicate that it is likely not a good idea to branch off that branch, because its history might change completely at any time.

A "topic" branch can be created on whichever branch is most easily worked upon in the local context. Great care must be taken though to make sure that the changes created to implement the new fix or feature do not depend on any other local changes and only depend on content from the remote repository – i.e. new features or fixes to be promoted upstream must be designed to work without any other local changes.

git checkout local-mynewfeature-v1.0 v1.0
# edit-compile-test-commit-loop

Now merge the new feature branch onto the local (previously created, see Create a local branch for your changes from the origin/master branch above) release branch:

git checkout local-v1.0
git merge local-mynewfeature-v1.0 --no-ff

Also merge it onto other locally-supported branches:

git checkout local-v2.0
git merge local-mynewfeature-v1.0 --no-ff

Finally merge it onto the local branch (also previously created) which tracking the remote trunk (master) so that it can be tested as a change set to be sent upstream:

git checkout local
git merge local-mynewfeature-v1.0 --no-ff
git format-patch -n HEAD~

14.2 Using git diff to see what's on a branch

The easiest way to see all the changes on a given branch is to switch to that branch and then run git diff with the name of the ref that specifies the base point of the branch. For example if a branch local-v2.0 was created from a release tag/branch v2.0 then you simple switch to the branch and run the diff:

git checkout local-v2.0
git diff v2.0

If you don't want to change the state of the working directory you can also do the diff by specifying the entire range of the branch:

git diff v2.0..local-v2.0

14.3 Using git merge to merge changes

The easy way to move all the changes from one branch to another is of course to use git merge to merge them into a commit on the new branch. Here's an example of moving all the changes from the local branch to a new local-v2.0 branch created from the reference v2.0 release branch/tag:

git checkout -b local-v2.0 v2.0
git merge local --no-ff		# or "local-v1.0", etc.

This creates one commit (on the new local-v2.0 branch) containing the entire range of changes from the entire local branch.

14.3.1 handling conflicts during git merge branch migration

If a conflict is detected when git merge runs it will have cleanly merged the non-conflicting changes and updated both the INDEX file (staging area) and the working directory; and for conflicting changes it will have recorded copies of the conflicting revisions of the files that could not be merged in the INDEX file (the staging area) and inserted 3-way merge conflict markers in the files with conflicts in the working directory.

You can now do one of two things; decide not to merge, or resolve the conflicts:

14.3.1.1 backing out the merge

To undo the merge one uses git reset --hard.

14.3.1.2 resolving merge conflicts

Look for files containing conflicts with git diff, and edit those files to resolve the conflicts, then git add the changes to the INDEX file (staging area).

git diff
# edit and resolve (and test)
git add
# repeating above steps as necessary

When all conflicts have been resolved (and tested), you may complete the merge by committing it:

git commit

14.3.2 backing out of a successful merge

If you wish to undo a successful merge, immediately after it was done, then you can use git reset --hard ORIG_HEAD

15 Submitting patches back upstream

Eventually you may wish to push your changes back upstream. This is most easily done per topic branch with git send-email.

(Note that some packagers, including Mac OS Fink, package this command as a separate add-on package)

15.0.0.1 Configuring for easy git send-email use

First thing to do is configure your working repository to make this command easy to use:

git config sendemail.to "Project Developer's List <project-devs@example.com>"
git config sendemail.chainreplyto true
git config sendemail.thread true

If your local host doesn't run a mail server that can deliver to the public Internet then you'll need to add some additional settings to tell Git how to send mail. The following may guide you:

git config sendemail.smtpserver 10.0.1.128
git config sendemail.smtpport submission
git config sendemail.smtpuser USERNAME
git config sendemail.smtppass PASSWORD
git config sendemail.smtpencryption ssl

15.0.0.2 Selecting changes to send upstream

The way you managing branches locally will determine how you specify the changes to be sent by git send-email.

If you are using topic branches then you will send one set of patches for each topic branch. You may be asked to rebase your topic branch to the head of the remote master branch if you haven't been keeping your local repository up-to-date. You should do this.

There are tools like topGit which can help with managing a slew of branches, including the ability to manage dependencies between them.

If you are just keeping all your local hacks on one branch, and you want to submit everything upstream, then the easiest thing to do is to use the name of your branch, along with the name of the branch it was made from (normally master), as the "rev-list":

git send-email --compose master^..local-work

(note the use of "^" – the meaning of master alone would miss the initial commit as it reads "since_ master")

Fill out the headers and write the body of the introductory message, and then all your changes will be on their way!

Note you can use the --dry-run option to see what will happen without actually sending any mail.

Note also that if you're working in a clone of a git svn cloned repository then you will probably have an initial commit containing all the .gitignore files created from SVN. In that case you would use master..local-work instead of master^..local-work because you do want to ignore that initial commit – the SVN maintainers upstream won't be interested in your .gitignore files!

(more needs to be written on this topic!)

16 Using A Shared Central Git Repository

Often it is useful to make use of Git in a manner similar to the way centralised VCS works.

16.1 Working on Shared Branches

Use git up to update all locally created tracking branches!

Typically one will want to create a new branch from master to work on something like a new feature to be merged at a later date, and to push this branch to the central server (origin) and then pull any changes from other developers into the local branch, thus this new branch must be a remote tracking branch.

Unfortunately there is no easy way to do this with Git directly in one command, though Magit makes it a wee bit easier.

  • create a "spin off" branch: This is the only way to create and check out a new branch without first stashing any local changes.
  • unset the upstream: first just unset it
  • set the upstream: then set it to the upstream name of the new branch, i.e. origin/new-branch-name (if you're following these steps in order you'll still be in the branch pop-up menu, so you don't need to press b again)
  • finally set the push remote too
M-x magit-status <RETURN>
b s feature-branch-name <RETURN>
b u
u origin/feature-branch-name <RETURN>
p

17 A handy Git update helper: git up

Install PyGitUp!

pkgin install py39-pip	# python version as appropriate...
pip3.9 install git-up		# now gitup!

Now you can update all of your local branches at once:

git up

18 Dealing with upstream history rewrites

When the upstream (central/shared) repository suffers history rewrites on the branch you're using, then plain git fetch && git pull will fail, sometimes leaving quite a mess.

Use of git clean might be necessary, followed by re-creating the local branch you are using, surrounded by first stashing local changes:

git add un-added-file.c
git stash
git reset --hard HEAD
git clean -f -d
git branch -m master OLDmaster
git checkout -f master
git branch -D OLDmaster
git reset --hard origin/master
git up
git stash pop

Dealing with submodules would go something like:

git add un-added-file.c
git stash
git reset --hard HEAD
git clean -f -d
git branch -m master OLDmaster
git checkout -f master
git branch -D OLDmaster
git reset --hard origin/master
git up
git submodule update
git submodule update --init --recursive
git submodule foreach git reset --hard HEAD
git submodule foreach git clean -f -d
: XXX maybe also rename and re-init the default branch???
git submodule foreach git submodule update --init --recursive
git submodule foreach git fetch
git submodule foreach git pull

19 Using Git with a remote Subversion (svn) repository

NOTE: See ~/notes/freebsd-svn-to-git for FreeBSD specifics, and also how to use svnsync to make a local SVN clone so that fetch doesn't take even longer than forever.)

There is an add-on Git command available called git svn which allows you to clone a Subversion repository and work on it locally with Git. (It's a big giant Perl script.)

(at least it's an add-on with "fink", and it is called "git-svn" – NetBSD pkgsrc has it in the default scmgit-base package)

Note that git svn is not an interface to Subversion per se, but rather a means of cloning from SVN. I.e. this is not a git remote helper add-on. This means if you want to make full use of Git with multi-level cloning of a repository derived from Subversion then you will have to implement a two-step process of first updating a bare Git repository from SVN, and then working with Git clones of that initial bare repository.

It is assumed that the upstream interface to the origin Subversion repository will be via patch submission, and not via git svn, though this process could probably be adapted fairly easily to be two-way either by pushing changes back to the bare Git repository and using git svn dcommit there directly, or perhaps by making a filesystem level copy of the bare repository and then converting it back to having a working directory and adding "remotes" to it to pull local changes from other local Git clones of the original repository, and then doing the dcommit's from there.

19.1 Cloning the SVN Repository Into Git

Use git svn to fetch and create a "clean" import of the entire SVN repository, and if you have a Git sever then do there:

mkdir project.svn-2-git
cd project.svn-2-git
git svn init -s http://svn.example.com/project
mv .git/* .
rmdir .git
git config core.bare true
git svn fetch --all
git branch -f -t master remotes/trunk

Some details:

The -s option tells git svn that the SVN repository is layed out the standard way (trunk is in trunk/, branches are in branches/, tags are in tags/). For anything non-standard, use --trunk=TheTrunk --tags=TheTags --branches=TheBranches as necessary. FreeBSD is an example of a project using non-standard layout for SVN branches and tags, indeed one with multiple branches directories, and the following might be an example of how to fetch the most commonly used ones: -b stable/ -b release/ -b releng/ -b user/ -t release/ -T head

(Note: Don't try to use the --prefix= option – it will only cause confusion and chaos.)

The git svn fetch --all step can take a very long time. Each change set is individually fetched from Subversion and converted to a Git revision. Omitting the --all option will fetch just the trunk, but except for the most basic uses that may not be sufficient, since for one you won't be able to properly make local topic branches from the actual release branches or tags that you may need to use in local production installs.

By default git svn fetch will have created a local branch named master, but that branch initially may be problematic, most importantly because if you've fetched all SVN branches then git svn will set master to track the most recently active SVN branch, which may not be the SVN trunk branch. Also, despite this being a "tracking" branch, it won't act like one due to this being a "bare" repository – you will have to manually re-point the master branch to the head of the SVN trunk after each git svn fetch.

19.1.1 Copying SVN tags to make them into Git tags

The following will add all the SVN "branch" tags as normal Git tags, which can be really handy if you want to refer to them later:

git show-ref | awk '$2 ~ /\/tags\// {
     n = split($2, ar, "/");
     tag=ar[n];
     printf("git tag -f %s %s\n", tag, $1);}' | sh

19.1.2 Creating .gitignore files

(perhaps git svn needs an option to put svn:ignore properties into the .git/info/exclude file, but that file is not cloned…)

By default git svn doesn't create .gitignore files from the SVN svn:ignore property list so if you build directly into a Git working directory then you'll end up with product files that Git neither ignores nor knows what to do with.

mkdir /work/$USER
cd /work/$USER

git clone --no-hardlinks /pub/project.svn-2-git project
cd project

Now copy the [svn-remote] section from /pub/project.svn-2-git into your new .git/config then copy the svn sub-tree from the parent repository into this new clone's configuration directory:

cp -R /pub/project.svn-2-git/svn .git/svn

# hmmm.... which branch should these be on???  probably:
git checkout master

git svn create-ignore
git commit -m "adding .gitignore files created by 'git svn create-ignore'"

Now you should be able to remove the config entries and the svn config directory you copied, or just remove the whole clone.

Now, how the heck do we keep these up to date from SVN??? Do we have to re-generate them every time we update from SVN?

19.2 Automating the SVN update process

Set up cron to do this regularly (or else remember to do it before you go do any work in any other local clone of this repository):

cd /pub/project.svn-2-git && git svn fetch --all
cd /pub/project.svn-2-git && git branch -f -t master remotes/trunk

Note that we are manually resetting the master branch in lieu of doing a pull operation because this is a "bare" repository (where a pull request cannot be done)

Important! To add (and/or reset) any new/changed SVN tags, you may have to run the tag copying procedure above again as well.

Perhaps you will wish to create a small script that does all three operations in sequence and then call that script once, periodically, from cron. You may wish to include an interlock in the script to prevent simultaneous invocations from tangling together.

Also, there's the issue of re-running git svn create-ignore

# WARNING:  this script could fail to interlock properly if it can be
# invoked simultaneously with more than one name!  Don't do that!
# 
LOCKDIR=/tmp/$(basename $0).lock

if [ -e $LOCKDIR ] ; then
      echo "It appears there's already an instance of $0 running..." 1>&2
      exit 1
fi

if mkdir $LOCKDIR ; then
      : #got it!
else
      echo "Oops, just missed grabbing $LOCKDIR!" 1>&2
      exit 1
fi

# do the Git stuff...

rmdir $LOCKDIR
exit $?

19.3 Working with the new Git cloned repository

At this point the newly created Git repository, cloned from a Subversion repository, can be treated as a remote read-only repository which can be cloned and worked on normally.

Each local developer will clone it to a working repository somewhere:

mkdir /work/$USER
cd /work/$USER

git clone --no-hardlinks /pub/project.svn-2-git project

# OR:

git clone git://git-server.local/pub/project.svn-2-git project

# and then
cd project
git branch local-work

This is now where you do all your feature & fix branch development and integration, etc.

Periodically you will want to update from your remote origin repository, just as you would from any normal upstream Git repository, pull forward your local master branch, and then either git merge or git rebase on your local topic branch(es) as necessary; and so on. You probably don't want to have any uncommitted changes in your working repository at that time.

git checkout master
git fetch -v --all
git rebase

then depending on what your local branch structure looks like you first need to switch your repository back to your local working branch with:

git checkout local-work

and then either merge the new changes on the master branch to it:

git merge master

or maybe, if you haven't published (pushed) your changes to another Git repository you can rebase the your branch onto the new head of the master branch with:

git rebase master

19.3.1 more about managing local changes

See the section Git Development of New Features/Fixes with Topic Branches above for more details…. However note that you probably don't ever want to merge topic branches to the master branch when working on a project that's ultimately hosted upstream in SVN. Instead you should create a local "integration" branch, either from the origin/master, i.e. the remote SVN "trunk" branch, or else from the origin/"release" branch you are interested in deploying. In that integration branch you will only perform merges from your local-work changes branch, or your local topic branches (and possibly other upstream topic branches). The head of the integration branch can be tagged as your local release.

Yes, this is a lot of work. Sigh.

See also the section Submitting patches back upstream above for suggestions about sending some or all of your local changes back upstream via e-mail.

19.3.2 Fetching additional SVN branches

You may notice that cloning the Git cloned SVN repository doesn't pull all the SVN branches into the secondary Git clone. If you need to refer to non-trunk branches from SVN, then configure your local Git clone repository to fetch all the origin branches like this:

git config --add remote.origin.fetch '+refs/remotes/*:refs/remotes/origin/*'
git fetch -v --progress --all

There is also a git imap-send command which may be used instead. It is used in conjunction with the git format-patch --stdout command.

Copyright (c) by Greg A. Woods, All Rights Reserved

Footnotes:

1

The ease of testing a change in isolation with other changes will of course depend on the project and just how invasive those other unrelated changes are. You may have to rebuild a project nearly from scratch if you undo local changes in places where they are at the top of the dependency tree, e.g. especially in a top-level config file.

Date: $Revision: 1.17 $ $Date: 2019/11/09 23:38:43 $

Author: Greg A. Woods

Created: 2024-01-27 Sat 10:32

Validate