44

Is it possible to commit a file in a git branch without checking out that branch? If so how?

Essentially I want to be able to save a file in my github pages branch without switching branches all the time. Any thoughts?

I needed to do this atomically without changing the current directory, so multiple commands won't work, even if they can be used as a one liner. Otherwise, you can end up with race conditions.

Update: It's impossible to do what I want (see comments below for use case). I ended up programmatically cloning my current directory to a tmp directory, then checking out my branch in that tmp directory (doesn't affect my working directory) and committing my files to the tmp directory clone. When I'm done, I push back to my working directory and delete the tmp directory. Sucks, but it's the only way to commit files to another branch without changing the current working branch of the working directory. If anyone has a better solution, please feel free to add it below. I'll accept yours if it's better than 'it cannot be done'.

Update update: For full details of why I asked to be able to do this 11 years ago and why I wanted it to be atomic see this repo https://github.com/schneems/git_test. Back in the day we didn't have CI services. Developers at the company I worked for were expected to run tests locally, but not everyone did it. The idea behind that project was to track test run history. This would allow you to see which commit tests (or if they weren't run at all).

CI now handles this functionality but since I asked an "impossible question", I'm always curious if there is an answer.

Schneems
  • 14,918
  • 9
  • 57
  • 84
  • 1
    Why don't you want to checkout another branch? Is it because you have uncommited changes? – gustavotkg Oct 28 '11 at 18:12
  • There's no way to do something like a `git commit -b branch_name ...`. – wkl Oct 28 '11 at 18:23
  • 2
    @gustavotkg i'm programmatically syncing files to a github pages branch in the background. I need to be able to do so without checking out the branch, since checking out the branch would affect the current users working branch. Now i'm doing a complicated dance of clones and tempdirs and pushes when all i really want to do is to add a single file without affecting the user's current branch. – Schneems Oct 28 '11 at 18:26
  • 8
    One use-case is, that I found a bug, made a quick fix, but want to push it into `develop` instead of the current issue branch. At least thats the reason why I missed that feature ;) – KingCrunch Oct 28 '11 at 18:27
  • @KingCrunch: Committing like this, without compiling or testing, is a dangerous development practise – CharlesB Oct 28 '11 at 18:29
  • 1
    @Schneems You could have another copy of the repo for you background usage and another for user usage. So you can push background's repo changes to the user working repo using referenced branches – gustavotkg Oct 28 '11 at 18:30
  • 3
    @KingCrunch you can use `git stash`, `git checkout develop`, fix your bug, `git commit`, `git checkout -` and then `git stash pop` so you get your changes back – gustavotkg Oct 28 '11 at 18:31
  • @Schneems: Looks like you're doing it the wrong way, please add more details to this workflow, I don't quite understand what you do exactly – CharlesB Oct 28 '11 at 18:32
  • 2
    There's nothing wrong with doing something in a clone; if they're local, it'll use hardlinks and you don't even take up extra disk space besides the extra work tree copy. If you don't want to have to push/pull, you could use [`git-new-workir`](https://github.com/gitster/git/blob/master/contrib/workdir/git-new-workdir) to make two work trees share the repo via symlinks in .git. – Cascabel Oct 29 '11 at 20:46
  • For me, the best way to do that is to keep a second clone with the right branch checked out in. Since the .git directory is compressed and the pages branch often a ligth one, it wouldnt consume too much disk space, if thats ever a concern. And so the only thing you would have to do is switch to the right directory. No need to push that to the other clone and make a "complicated clone dance", just push it online as usual, and you could eventually sync the other clone by a simple fetch. – Jean-Bernard Jansen Apr 01 '14 at 22:28
  • I work on a fairly large repo, and I have used [CB Bailey's solution](https://stackoverflow.com/a/7941509/184546) to this question multiple times with great success. IMHO you should consider changing the accepted answer because it does exactly what your question asked; it's absolutely possible! It also happens to be what I wish to do sometimes, which is add a commit to another branch without checking it out, cloning, or creating a new worktree, because any of those would take a long time, comparatively. – TTT Jun 07 '21 at 18:55
  • @CharlesB it's not so dangerous if you have a CI server that automatically builds and tests any commits you push (and, not having such a server is dangerous for major projects) – Andy Jun 14 '21 at 19:23
  • In addition to CB Bailey's answer, [Peter's answer](https://stackoverflow.com/a/66850396/184546) also provides a mechanism for doing this without using extra disk space with multiple clones or worktrees, which IMHO are truer to the spirit of the original question. (I'm coming from the POV of a large repo where additional clones or worktrees are undesirable.) – TTT Jul 27 '22 at 19:37
  • My original question needed to be able to do this atomically. I realized I didn’t specify. I updated the question along with the full context of why I wanted it in the first place. Running multiple commands in a long lived “test” process might conflict with what the developer is doing locally (race conditions) so I wanted to avoid it. – Schneems Sep 10 '22 at 13:11

11 Answers11

21

It's not possible.

The changes you commit are related to the current working copy. If you want to commit to another branch it means that you could commit changes from your working copy, but base them from another copy state.

This is not a natural way of versioning your work, and this is why you need to make different steps (stash changes, checkout the branch, pop stash and commit) to accomplish it.

As for your specific use case, a simple way is to keep two copies of your work, one checked out at master branch, and the other at pages branch.

In the pages working copy, add the master copy as a remote repo.

  • You commit pages on master
  • Pull from master on the pages copy
  • push to GitHub
  • reset the master branch at its previous state.
CharlesB
  • 86,532
  • 28
  • 194
  • 218
  • 1
    It's possible to read files from a different branch without checking it out, so I was hoping there would be a way to write to a different branch without checkout. While i understand this isn't 'natural' i'm using GIT as a data store and not as pure version control in this situation. I have a workaround (see my comment above) but it is rather gross and prone to errors. – Schneems Oct 28 '11 at 18:44
  • Just curious: why maintain a separate pages branch? – CharlesB Oct 28 '11 at 18:51
  • 2
    http://pages.github.com/ it's a way to serve html content such as docs or a demo page directly using github. It has to be in a separate branch because thats the way github wrote it. While the data (docs in this case) is related to the project it shouldn't necessarilly be in the main branch for a number of reasons, overwhelming the commit messages of master with changes unrelated to code being one of them. – Schneems Oct 28 '11 at 18:58
  • See answer by Charles Bailey below! – jbenet Jan 07 '13 at 10:19
  • 1
    How is this answer correct? The changes you commit have nothing to do with your working copy. They're based entirely on what is added to the index. You can delete everything in your work tree and still `git commit` as if nothing had happened, because git only looks at the index. – user541686 May 31 '18 at 05:31
  • _This is not a natural way of versioning your work_ - its a natural way of wanting to backup work in progress periodically without having to fuss with branch switching and cherry picking – StingyJack Jul 08 '20 at 19:59
18

As several others have said, it is literally possible, but impractical.

However, as of Git 2.5 (with some important fixes in 2.6 and minor ones since then), there is a practical method for doing this using git worktree add.

Let's say, for instance, that you want to work on branches main and doc "at the same time", or branches develop and test "at the same time", but the two branches in question deliberately contain different things. (For instance, the doc branch has documentation that exists outside or alongside the code, or the test branch has tests that will be run against the code, but not distributed, or which are expected to have failures for which tests are deliberately skipped on the develop side, or whatever.)

Instead of just:

git clone -b develop <source> theclone

followed by working in theclone with constant switching back and forth between the two branches, you would:

git clone -b develop <source> theclone

but then:

cd theclone
git worktree add ../ct test  # check out branch test in ../ct

or just:

git worktree add ../test     # check out branch test in ../test

Now you can run your tests in ../test while developing in theclone. You can merge and/or rebase changes from one branch to the other in the usual way: the underlying repository is already shared, so no git push or git fetch is required. You simply have both branches checked out, into two separate work-trees, named theclone and test from the top level.

torek
  • 448,244
  • 59
  • 642
  • 775
14

It can be done by reimplementing git commit.

This can be done with various call to git hash-object

But this is hard to achieve.

Please read progit chapter 9 for more details and a full example of how to simulate a commit.

CharlesB
  • 86,532
  • 28
  • 194
  • 218
p'tit fred
  • 191
  • 1
  • 4
13

So long as you don't have anything in your current index that differs from your HEAD that you want to keep you can so something like this. (If you do want to keep your index you could temporarily export the GIT_INDEX_FILE environment variable to point at a temporary file for the duration of these commands.)

# Reset index and HEAD to otherbranch
git reset otherbranch

# make commit for otherbranch
git add file-to-commit
git commit "edited file"

# force recreate otherbranch to here
git branch -f otherbranch

# Go back to where we were before
# (two commits ago, the reset and the commit)
git reset HEAD@{2}

We've never actually checked out otherbranch and our working tree files haven't been touched.

CB Bailey
  • 755,051
  • 104
  • 632
  • 656
  • I wonder if this solves a related problem as to *why* you don't want to checkout another branch (or clone a copy of the repo): Suppose you have a very large repo and "otherbranch" is wildly different from your current branch (but still relatively close to the merge-base), and you want to add a minor hotfix to it. Checking out otherbranch, making the change, and going back to your working branch could set you back quite some time. Is it correct to assume that *reset --mixed* as presented in this approach would be significantly faster than checking out a very old branch? – TTT Jul 23 '20 at 19:10
  • This really ought to be the accepted answer. Today I had to create a branch from a tag from a year ago, and then add one commit to it. I tested it both ways. The branch differed from my current head by 72,000 files, and it took me 2.5 minutes to checkout. (Going back only took 1.5 minutes.) Then I tried your way and it only took about 15 seconds each way, most of which was the time it spent printing out the list of files. For larger repos, file sizes, and/or slower hard drives (mine's a speedy SSD), the difference would likely be more drastic. This is a great solution! – TTT Sep 10 '20 at 06:13
6

Most answers here say that it can't be done, or use stashing. It can be done without stashing, but it's not easy and it's not something you want to incorporate in your daily routine, I think.

For this example, I started with a main branch and a foo branch.

o--o--o (main)
    \
     o (foo)

I want to commit a file to main while foo is checked out. The commands I write here are all done without checking out main.

git add examplefile
git write-tree  
    # this returns a hash: 2ceb75e10358ba27aaf2291e4e302d759fbedd55
git commit-tree 2ceb75e10358ba27aaf2291e4e302d759fbedd55 -p main -m "commit message"  
    # this makes a commit with commit the main branch points to as parent
    # it too returns a hash: 56823299cdc8b6c1ab30095f6b2c2143f3bb2122
git branch -f main 56823299cdc8b6c1ab30095f6b2c2143f3bb2122
    # this points the main branch to the new commit

So what did we do? Essentially, we did all the steps a normal add-commit routine does, with some tweaks.

First, we add a file to the staging area just as normal. Next, we run git write-tree. That writes the staging area to a tree and returns the hash of that tree. A commit is not much more than a pointer to a tree, a pointer to its parent(s) and a message.

Then, we actually make the commit by giving it the three things it needs: the tree, the parent (main) and the commit message. Note on my computer this command doesn't open an editor for the commit message, so I need to give it in the command line. Your experience may vary.

The situation is then as follows:

        o (new commit)
       / 
o--o--o (main)
    \
     o (foo)

We made the new commit at the right spot. However, it's dangling, meaning there is no branch (or tag) pointing to it. We now have to point main to the new commit.

The last command does exactly that. It takes the hash of the newly created commit and points main towards it.

Done!

As I said at the start: this is not something you want to do often. The normal add-commit strategy is well thought out. This strategy might mess up something you're not aware of. It doesn't check for conflicts, it just overwrites files without mentioning it.

Finally, a one-liner to do everything in one fell swoop:

git branch -f main $(git commit-tree $(git write-tree) -p main -m "commit message")

The hash output from write-tree is used immediately as an input for commit-tree, and its output is used immediately in moving the branch.

Peter
  • 2,932
  • 1
  • 12
  • 9
  • Nice one liner (after staging the changes). Note this method is very similar to [CB Bailey's answer](https://stackoverflow.com/a/7941509/184546) but your answer doesn't require the 2 resets. – TTT Jul 27 '22 at 19:17
  • But you add the file in the index.... as you have it in `foo`, right? Then the _tree_ will end up being like `foo` with the adeed file instead of `main` with the added file, right? If the worktree is clean perhaps you could do a `restore --staged source=main -- .` to get the index like `main`, then add, then `write-tree; commit-tree` and move the branch pointer? – eftshift0 Oct 19 '22 at 20:42
5

I made a little tool that does exactly this: https://github.com/qwertzguy/git-quick

It let's you edit specific files from another branch without checking out the other branch completely (just the files you want to edit) and commit them. All this without ever affecting your working copy or staging area.

Behind the scenes it uses a combination of git worktree and sparse checkout. The source is fairly small, so you can read through.

qwertzguy
  • 15,699
  • 9
  • 63
  • 66
4

I cannot agree it is not possible. By mixing git stash push, git stash pop, git checkout, git checkout, git add and git commit this is possible.

How I understand problem:

You are on branch master and you made some modifications to file patched.txt and you would like to commit this file to other branch.

What you would like to do is:

  • save all changes in this repo by doing git stash
  • checkout file.txt from stashed stack
  • add file patched (and only this file) to new branch
  • get back to state of repo before modyfing file.txt

This can be achieved by executing following commands:

destBranch=patch
thisBranch=master
FileToPutToOtherBranch="file1.txt file2.txt 'file with space in name.txt'"
message="patched files $FileToPutToOtherBranch"
                                                                                  #assumption: we are on master to which modifications to file.txt should not belong
git stash &&\                                                                     #at this point we have clean repository to $thisBranch
git checkout -b $destBranch &&\           
git checkout stash@{0} -- $FileToPutToOtherBranch &&                              #if there are many files, repeat this step                                         #create branch if does not exist (param -b)
git add $FileToPutToOtherBranch &&\                                               # at this point this is equal to git add . --update
git commit -m "$message" &&\
git checkout $thisBranch &&\
git stash apply &&\                                                               # or pop if want to loose backup
git checkout $thisBranch -- $FileToPutToOtherBranch                               # get unpatched files from previous branch

The reason why I am using "&&" and the end is if somebody will copy&paste this snippet into terminal, even if one error occurs, next commands will be executed, which is not good. \ is for informing shell that command is continued in next line.

To proove this works I provide testing environment for this snippet

mkdir -p /tmp/gitcommitToAnyBranch && cd /tmp/gitcommitToAnyBranch &&\
git init 
echo 'this is master file' > file1.txt
echo 'this is file we do not want to have modified in patch branch because it does not     patches any feature' > docs.txt
git add file1.txt && git commit -m "initial commit" 
echo 'now this file gets patched' > file1.txt
git status

Now, if you run my script with parameters

destBranch=patch
thisBranch=`git rev-parse --abbrev-ref HEAD`
FileToPutToOtherBranch="file1.txt"
message="patched file $FileToPutToOtherBranch"

You will have file1.txt modified only in patch branch, for more see gitk --all

test30
  • 3,496
  • 34
  • 26
3

While there is currently no single command to do this, there are at least two other options.

  1. You could use the github api to create the commit. This post details creating a commit in a github repo.

  2. Create github pages as a submodule.

  3. Use a series of plumbing commands to create the commit.
    The git book has a description of plumbing commands used to create a commit

note: the command is now mktree not mk-tree

CharlesB
  • 86,532
  • 28
  • 194
  • 218
codingFoo
  • 916
  • 12
  • 23
1

If you accidently modified things in the wrong branch, here's some simple steps :

  1. Commit these changes ;
  2. Merge them into the right branch ;
  3. Checkout the branch your were on at first and reset it to the commit before ;
  4. Cleanup your modifications with "git checkout -- .".

Everything should be fine after this. You can also merge, reset and cleanup your modifications selectively.

Arteymix
  • 11
  • 1
1

No you cannot, however what you are doing by hard copying files is implemented by git stash using commits. The stash works as a stack of changes that are saved "somewhere outside the branches spaces", hence can be used to move modifications between branches.

  • Make your changes on <file>
  • git add <file>
  • git stash
  • git checkout <branch you want to commit>
  • git stash pop
  • git commit -m "<commit message>"

you can probably scrip these simple commands down if you do this constantly.

-2

This is how I do it:

if I've acidentally committed, roll back one commit:

git reset --soft 'HEAD^'

add the files you want to add

git add .

create a new temporary branch:

git checkout -b oops-temp

commit your changes:

git commit -m "message about this commit"

checkout the branch you meant to check out:

git checkout realbranch

merge the old branch:

git merge oops-temp

delete the old branch:

git branch -D oops-temp
alana314
  • 633
  • 8
  • 11