Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

same global remote gitignore file across all branches

What I am trying to achieve is having one single .gitignore file (tracked by git) that is synced across all of the branches in the remote repository (hosted on GitHub) and thus also on the corresponding local branches. The .gitignore file I am currently using is not perfect so once in a while (sometimes multiple times per day) I have to update it. The problem then is that I have to checkout that .gitignore file across all branches manually which is becoming more and more a pain in the ass as more branches are created. So for every branch I do (with the updated .gitignore in branch master)

git checkout some-outdated-branch
git checkout master .gitignore
git add .gitignore
git rm -r --cached .
git add .
git commit -m "Updated .gitignore and fixed tracked files"

As this is relatively time consuming for multiple branches I tried to search for a way to have the one .gitignore file in branch master (or in a seperate gitignore-branch branch) that is automatically synced across all branches (locally, as well as remotely when pushed).
The problem here is that I do not want to use git config --global core.excludesfile /path/to/local/.gitignore (as suggested here) as I want my project partners to also use that specific .gitignore file and not have to change the git config file for this. In this comment someone else is asking this question, but it has not been answered. I can also not find any answers on Stack Overflow regarding my problem.

Short summary
I want to edit the .gitignore file on one branch only and have that change synced with all other branches (automatically) in a time and effort efficient manner. After that I want to push the change in all branches to the remote repository (preferably with only one or a few lines of code and not having to remake a commit with corresponding commit message for each branch).

like image 352
Aron Hoogeveen Avatar asked Oct 28 '22 22:10

Aron Hoogeveen


1 Answers

Unfortunately, as long as .gitignore (or indeed any file) is tracked (meaning in the index), a logically-separate copy of that file goes into every commit you make. The upshot of this is that it's not possible to achieve what you want.

The closest you can come is, as phd mentioned, to store, in each new commit, a .gitignore entry that is of type symbolic link (mode 120000 in Git-internal-ese). Then, even though each commit has a logically-separate (probably physically-shared) copy of the link's target pathname, when Git goes to read the contents of .gitignore it will read the contents of the target pathname, rather than of a .gitignore work-tree file that was just copied out of whichever commit you told git checkout to get out.

You can, however, automate the process of updating .gitignore files across multiple commits. The easiest way to do this is probably using git worktree add to create a separate worktree in which to do the updates. This assumes your Git version is at least 2.5 and preferably at least 2.15 (to avoid bugs in git worktree).

The following is a completely untested script that will, for each remote-tracking branch, make sure that the tip commit of that remote-tracking branch contains a .gitignore that matches the one in the current branch in the main repository, using an added work-tree. It uses the detached HEAD mode to achieve this (and also pushes more than one commit at a time, when appropriate). It does not deal properly with multiple remote names with a single URL; to do that, remove the git fetch --all and uncomment the obvious line in new_remote.

#! /bin/sh
#
# git-update-ignores-across-remote-tracking-branches

. git-sh-setup     # get script goodies, and make sure we're at top level

require_work_tree  # make sure we have a work-tree, too

# Where is our ignore file? (absolute path)
IFILE=$(readlink -f .gitignore) || die "cannot find .gitignore file"

# set up a temporary file; remove it on exit
TF=$(mktemp) || die "cannot create temporary file"
trap "rm -f $TF" 0 1 2 3 15

# Use a work-tree in ../update-ignores
if [ ! -d ../update-ignores ]; then
    [ -e ../update-ignores ] &&
        die "../update-ignores exists but is not a directory"
    git worktree add ../update-ignores --detach ||
        die "unable to create ../update-ignores"
else
    # Should use git worktree list --porcelain to verify that
    # ../update-ignores is an added, detached work-tree, but
    # I leave that to someone else.  It might also be good to
    # leave remote-tracking names for other added work-trees
    # alone, but again, that's for someone else to write.
fi

# Find upstream of current branch, if we're on a branch and there is
# an upstream - we won't attempt to do anything to that one, so as to
# avoid creating headaches for the main work-tree.  Note that this
# sets UPSTREAM="" if the rev-parse fails.
UPSTREAM=$(git rev-parse --symbolic-full-name HEAD@{u} 2>/dev/null)

# Now attempt to update remote-tracking names.  Update all remotes
# first so that we are in sync, then list all names into temporary file.
# From here on, we'll work in the update-ignores work-tree.
cd ../update-ignores
require_clean_work_tree "update ignores"
git fetch --all || die "unable to fetch --all"
git for-each-ref --format='%(refname)' refs/remotes > $TF
REMOTE=
UPDATED=

# Function: push UPDATED to REMOTE.  Set REMOTE to $1 and clear UPDATED.
# Does nothing if UPDATED or REMOTE are empty, so safe to use an extra time.
new_remote() {
    local u="$UPDATED" r="$REMOTE"
    if [ "$u" != "" -a "$r" != "" ]; then
        git push $r $u || die "failed to push!"
    fi
    UPDATED=
    REMOTE=$1
    # [ -z "$REMOTE" ] || git fetch $REMOTE || die "unable to fetch from $REMOTE"
}

while read name; do
    # skip the upstream of the main repo
    [ $name == "$UPSTREAM" ] && continue
    # Update this branch's .gitignore, and remember to push this commit.
    # If we're switching remotes, clean out what we've done so far.
    shortname=${name##refs/remotes/}  # e.g., origin/master or r/feature/X
    remote=${shortname%%/*}           # e.g., origin or r
    branch=${shortname#remote/}       # e.g., master or feature/X

    # if we're changing remotes, clear out the old one
    [ $remote != $REMOTE ] && new_remote $remote

    # switch detached HEAD to commit corresponding to remote-tracking name
    git checkout -q $name || die "unable to check out $name"
    # update .gitignore (but skip all this if it's correct)
    cmp -s .gitignore $IFILE 2>/dev/null && continue
    cp $IFILE .gitignore || die "unable to copy $IFILE to .gitignore"
    git add .gitignore || die "unable to add .gitignore"
    # UGH: terrible commit message below, please fix
    git commit -q -m "update .gitignore" || die "unable to commit"
    commit=$(git rev-parse HEAD) || die "failed to rev-parse HEAD"
    # remember to push this commit (by hash ID) to refs/heads/$shortname
    # on $REMOTE (which is correct because of new_remote above)
    UPDATED="$UPDATED $commit:refs/heads/$shortname"
done < $TF
# push any accumulated commits, or do nothing if none accumulated
new_remote

# and we're done!
exit 0
like image 171
torek Avatar answered Nov 10 '22 00:11

torek