You are here

Git tricks

Some tips for manipulating your git repo.

Splitting a subfolder out into a new repository

Instructions here.

Experimenting with changes using a copy of a repo

  • Clone the repo to have a local copy.
  • cd into that repo root directory
  • remove the remote
  • git remote rm origin

Removing the remote ensures that any changes you make to the files and history won't accidentally be pushed back to the remote.

Creating a repo to be populated with content from another repo

  • On Github, create a new repo, but don't create a README file, you want the repo to be empty.
  • Once you've populated the repo, either with new files or files copied with history from another repo, add and commit the changes,
  • then push to the remote (you may need to force push: git push master origin -f).

Rename or move files or folders, with and without retaining history

Moving or Renaming a folder (they're equivalent) "Without history"

original source

Basic rename (or move):

git mv <old name> <new name>

Case sensitive rename—eg. from casesensitive to CaseSensitive—you must use a two step:

git mv casesensitive tmp
git mv tmp CaseSensitive

…followed by commit and push.

git mv does in fact retain the file's history, but not in a very useful way. If you look at the history of this new old file on GitHub, you won't in fact see the full history. I.e. The file (or all the files contained in the renamed/moved directory) history will only show back to this renaming commit. To see "past" that commit you'd need to "follow" it:

git log --follow myFile.txt

So if you want to rename a file or directory, you need to...

Move or rename a file, retaining its history

source

Note: the following procedure will overwrite git history. This is probably not a good idea for repositories with multiple contributors, be sure you understand the implications before proceeding.

There is a handy utility git-mv-with-history that you'll need to store in your ~/bin folder so that you can run it from the command line.

If the above link to the gist is no longer available, the script is also stored here.

Make sure your bin folder is in your PATH, in Terminal:

export PATH="$HOME/bin:$PATH"

Note: This change is only temporary (it works only in the current session of the shell). To make it permanent, add the line to your .bashrc file located in your home directory.

Once you've downloaded the git-mv-with-history script and put it in your ~/bin folder, run the command:

git-mv-with-history README.txt=README.md  \     <-- rename to markdpown
                    src/help.txt=docs/    \     <-- move help.txt into docs
                    src/makefile=src/Makefile   <-- capitalize makefile

This example shows three different kind of operations, as described in the comment on each line.

Note: the name of the actual command in the script description is incorrect, it shows the example command as "git-rewrite-history".

Move or rename a folder, retaining its history

Notice that the above script is working on individual files. To rename a folder or move it, it's a bit trickier. It's cumbersome, but this way is tested and works: for renaming a directory, you'll basically move files into a temporary directory (rewriting the history), then move the files back to the (renamed or moved) directory, again writing the history.

For example, if I have a bunch of files in a folder called "classes", and I want to rename that folder to "Classes" (uppercase), it involves 2 operations:

1. move all the files to a temp folder:

git-mv-with-history \
    classes/RotaryView.sc=tmp/ \
    classes/ValueView.sc=tmp/ \
    classes/ValuesView.sc=tmp/

2. move all the files into a new folder, with the new name (it doesn't need to already exist, it will be created if it doesn't already exist)

git-mv-with-history \
    tmp/RotaryView.sc=Classes/ \
    tmp/ValueView.sc=Classes/ \
    tmp/ValuesView.sc=Classes/

When pushing these changes to your remote, you'll probably need to force push: git push master origin -f.

CAUTION: this is rewriting the history of the repo, so be careful using this if others are also doing work on the repo!

Copy a file or directory from another repository preserving the history

original source

This makes a git format-patch for the entire history of the file or subdirectory that we want and then import it into the destination repository. 

In Terminal, from your home directory:

  1. mkdir /tmp/mergepatchs
  2. cd ~/repo/subFolder
  3. export reposrc=myfile.c      #or mydir
  4. git format-patch -o /tmp/mergepatchs $(git log $reposrc|grep ^commit|tail -1|awk '{print $2}')^..HEAD $reposrc
  5. cd ~/repo/destinationFolder
  6. git am /tmp/mergepatchs/*.patch

This makes a patch file containing the file/directory (and history) of whatever you set as reposrc, that is merged into your target repository.

Note if you do this multiple times, you'll want to remove /tmp/mergepatchs after each time you run this process,

  1. cd ~
  2. rm -r /tmp/mergepatchs

so that it creates a fresh merge patch each time (otherwise you'll accumulate merges, which could be a good thing for copying multiple things at once, though I haven't tested this).

When pushing these changes to your remote, you may need to force push: git push master origin -f.

Erasing a file or directory and its history from a repo

Caution: this rewrites the history of the repo (changing commit hashes, etc.)

original source

# Make a fresh clone of YOUR_REPO
git clone YOUR_REPO
cd YOUR_REPO

# Create tracking branches of all branches
for remote in `git branch -r | grep -v /HEAD`; do git checkout --track $remote ; done

# Remove DIRECTORY_NAME from all commits, then remove the refs to the old commits
# (repeat these two commands for as many directories that you want to remove)
git filter-branch --index-filter 'git rm -rf --cached --ignore-unmatch DIRECTORY_NAME/' --prune-empty --tag-name-filter cat -- --all
git for-each-ref --format="%(refname)" refs/original/ | xargs -n 1 git update-ref -d

# Ensure all old refs are fully removed
rm -Rf .git/logs .git/refs/original

# Perform a garbage collection to remove commits with no refs
git gc --prune=all --aggressive

# Force push all branches to overwrite their history
# (use with caution!)
git push origin --all --force
git push origin --tags --force

DIRECTORY_NAME/ should be replaced with nameOfYourDirectory/ or nameOfYourFile.txt (no trailing slash).

Note: the line "# Create tracking branches of all branches" didn't work for me (posted an error), but it didn't seem to matter and subsequent lines worked fine.

You'll need to run this full process (after the cloning) again for multiple folders or files (i.e. you can't run the git filter-branch command multiple times to accumulate files or folders to remove).

You can check the size of the repository before and after the gc with:

git count-objects -vH

Cherry picking

source

You want to cherry pick a commit or merge on one branch, onto another branch. Checkout the branch you would like to cherry pick onto. If this is a branch which you'll eventually be submitting a pull request on to some remote, you'll probably want to branch from here to a new branch, named something like cherry-originalMergeID. E.g. if you're trying to cherry pick merged PR #213 from the Supercollider develop branch onto the 3.10 release branch, first 

git fetch upstream // fetch from your upstream remote

git checkout 3.10 // switch to your local branch that you'll be ultimately looking to merge the cherry pick into

git pull upstream 3.10 // update your local branch with any new changes from the upstream remote

git checkout -b cherry-213 // create and checkout your new branch to cherry pick into

git cherry-pick -m 1 <hash> // do the cherry pick

Now you're ready to publish/push this to your remote and create a PR to the upstream remote branch 3.10. Note if creating the PR with GitHub, you can select which branch you're trying to merge your change into, and it may default to another branch, like master, but you can select your target branch, 3.10, from the dropdown.

 If you're cherry picking only one or more commits, the command looks like

git cherry-pick <hash1> <hash2>

If instead of individual commits or a single merge you'd like a range of commits there are two options:

git cherry-pick ebe6942..905e279

or 

git cherry-pick ebe6942^..905e279

The first hash is the oldest commit, the last is the most recent. The first version picks the commit range beginning after ebe6942 up to and including 905e279. The second is inclusive of ebe6942 up to and including 905e279.