-2
  1. git checkout development
  2. git push <remote-origin> development
  3. git checkout <remote-origin>/development
  4. remove few files
  5. git checkout development
  6. modify same bunch of files
  7. git commit origin/development
  8. git push <remote-origin> development - fails, how to resolve conflicts. because the modified files aren't present in the remote-origin.

How can I push rest of the changes and ask git to skip files which aren't there in remote-origin?

torek
  • 448,244
  • 59
  • 642
  • 775
Rpj
  • 5,348
  • 16
  • 62
  • 122

1 Answers1

2

How can I push rest of the changes and ask git to skip files which aren't there in remote-origin?

You can't. You don't push changes, nor do you push files. What you push are commits. By thinking of Git as being about files, or changes, you've taken a wrong turn—way back at your step #2 in fact—and created a pretty big set of headaches for yourself. But the commands you showed here aren't the commands you actually used, so I can only really guess.

What to know in order to proceed

Git is, in the end, all about commits. Branch names—to the extent that you use them—are there to help you, and Git, find specific commits. It's possible to get along without them for a while, but because the actual commit numbers are horrible and impossible for humans to work with, things are much more pleasant when using branch names. So that's what we'll do. But still, Git is about the commits, not the branch names.

Git is also not about files in a key way here. Each commit holds files, but Git is about the commits. You either have a commit—in which case you have all of the files that are in that commit—or you don't, in which case you have none of those files. The thing about the files that are in the commits, though, is that they're not very usable.

To understand what's going on, then, we need to talk about commits. What exactly is a commit, in Git? How do we find a commit? How do we use a commit? How do we go about making new commits? So let's do that now.

What, exactly, is a commit and how does Git find one?

A commit, in Git, is a numbered entity. The numbers aren't simple counting numbers though: we don't have commit #1, then commit #2, and so on (or 0, 1, 2, etc., though commit reserves the zero value for "no commit"). Instead, each commit has a very large, random-looking number, between 1 and 2160-1. This number is normally expressed in hexadecimal, e.g., cefe983a320c03d7843ac78e73bd513a27806845.

Every unique commit gets a unique number. Two Gits, meeting over the Internet to exchange commits with each other, can therefore tell, just by these numbers, which Git already has which commits. So if you clone a repository—which copies all of its commits—and then add two or three new commits, it's now easy for you to have your clone give only the new commits to the original (or origin) repository.

So, Git will find a commit by this commit number. Git really needs this number, but as you have already seen with some git log output for instance, those numbers look random and are impossible for humans to remember. (They are actually not random: they are the outputs of a cryptographic hashing function. That's how and why two different Git software installations, using a repository, can produce the same hash IDs for the same commits, and different hash IDs for different commits.) We will use names—branch names and other names—to find the numbers, later; for now, let's move on to what's in a commit.

Each commit consists of two parts:

  • One part of any commit is its snapshot. This snapshot contains a full, but frozen-for-all-time, copy of every file as of the state it had when you, or whoever, said "commit these files".

    You can think of this as being a lot like a tar or zip archive, because it is a lot like a tar or zip archive. But the files inside this snapshot are in a special, read-only, Git-only, compressed and de-duplicated format, that only Git itself can read, and literally nothing can write. (That's a bit different from archive formats, which usually many programs can read and some can update in place.)

  • The other part of a commit, separate from the snapshot, is the metadata, or information about the commit itself. Here you'll find the name and email address of the person who made the commit. There are some date-and-time stamps as well, and the person who makes the commit can insert a log message saying why they made the commit. So this metadata is most of what git log shows you about each commit, if you run git log with no options.

    Contained within that metadata, Git stores some information purely for Git's own internal use. In this area, Git can store the raw hash IDs of any number of previous commits. Most commits—the ones we call ordinary commits—store exactly one previous commit. We call this the parent of the commit.

By storing one parent commit hash ID inside each commit, Git arranges for those commits to know "how they came about", as it were. That is, suppose we have a simple linear chain of ordinary commits, made one right after the other. Each one will have some big ugly hash ID as its commit number, but to draw them, let's substitute in single uppercase letters, like this:

... <-F <-G <-H

Here H stands in for the hash ID of the last commit in the chain. If we know the actual hash ID for H, and give that to Git, Git can find the commit (provided we have it). That gives Git access to the read-only snapshot—all the files, saved for all time—and also to the metadata.

By reading the metadata, Git can now find the hash ID for earlier commit G. Git can use that to find the actual commit itself, which makes it possible for Git to access the earlier snapshot: all the files, saved for all time.

If we have Git extract both the G and H snapshots (to a temporary area in memory) and compare them, Git will be able to find out which files, if any, are changed between G and H. If there are new files, or files removed, Git will find that as well. Git can then present this information to us as *what changed in H.

Note that commit H does not store changes. Commit H stores a full snapshot. Commit G also stores a full snapshot. It's only by comparing the two that Git finds changes—and yet, Git only needs the information about H, the hash ID, to find both G and H.

Having shown H as a diff—as changes since earlier commit G—a program like git log -p can now walk back one step to commit G. This has a snapshot and metadata, just like H did. The metadata for G include the hash ID of earlier commit F, so Git can extract the snapshots for F and G and use that to figure out what changed in G. So git log -p can show us G's author and message and a diff here, too.

Having done that, git log -p can once again walk backwards one step to commit F. Like every commit, this has a snapshot and metadata, so git log can repeat the process, over and over. All we need to do is—somehow—point Git to the latest commit H. From there, H points backwards to G, G points backwards to F, and so on.

Branch names help us find commits

The one fly-in-the-ointment above is that we have to somehow provide H's hash ID to Git. We could memorize it, or write it down on paper or a whiteboard—but we have a computer. Why not have the computer save it?

This is what Git does. Git uses names—branch names, tag names, remote-tracking names, all kinds of names really—to store commit hash IDs. All of these names store just one hash ID, but that's sufficient, because the commits themselves also store hash IDs.

Branch names, however, have one other special feature, that the other kinds of names don't. We can use git checkout or git switch to pick out one particular branch name and make it the current branch name. So:

git checkout feature

will look for the existing name feature in our Git repository. Assuming that name does exist, Git will set things up so that feature is our current branch and will extract, from the snapshot for the latest commit that is "on" that branch, the files that go with that commit. We can draw that like this:

...--G--H   <-- feature (HEAD)

The special name HEAD, written in all uppercase like this, is attached to one branch name. That one branch name then points to commit H. (More literally, the branch-names database says that the hash ID associated with that name is the hash ID H, whatever it really is.)

Note that we can have more than one branch name pointing to commit H. If H is the latest commit on both main and feature, we'd draw that like this:

...--G--H   <-- feature (HEAD), main

(assuming we're using the name feature to locate commit H right now). Both names select commit H, so it won't matter right now which one we use. Commit H points backwards to commit G, which points backwards in turn: I've just gotten a bit lazy about drawing the internal arrows going from commit to commit here. Those can't be changed, but the branch name arrows can be changed.

Making commits useful

I mentioned earlier that commits, once made, are frozen for all time. Well, actually I only said that about the files inside the commit, but it's true for the other parts of each commit too. No part of any commit can ever be changed (because of the cryptographic hash trick).

In order to actually use the files, then, Git has to "thaw them out" as it were: read out its internal-format, de-duplicated files, de-compressing them and making duplicate copies. These duplicates are in your ordinary computer format: they're actual files, with file names stored in folders, the way your computer works with them. (Git's internal files aren't stored this way.) So git checkout or git switch does just that.

After the git checkout or git switch finishes, you have, in a work area, all of your files, ready to work on/with. Git calls this working area your working tree or work-tree. These are copies that were extracted from some commit. These files, that you're going to work on, are not in Git. They are just here in your working tree, so that you can get work done.

Git could stop here, with just the two copies of each file:

  • There is the compressed, Git-ified, and de-duplicated copy in the current, or HEAD, commit.
  • And, there is the regular ordinary file copy in your working tree.

But unlike other version control systems, Git keeps a third copy of each file—or "copy", really—in between those other two copies:

  • There is the compressed, Git-ified, and de-duplicated copy in the current, or HEAD, commit.
  • Then, there's a second compressed, Git-ified, de-duplicated copy—or "copy"—in what Git calls its index.
  • And, there is the regular ordinary file copy in your working tree.

Initially, this second "copy" is the same as the HEAD copy. As such, it's de-duplicated away. So the index stores only the name of the file and a magic identifier—another hash ID, actually—to let Git find the contents in its de-duplicated files storage area.

When you change some file in your working tree, nothing happens in Git. So eventually you need to run git add on these files. This git add command tells Git: Read the working tree file. Compress it into the internal Git format, and check to see if the result is a duplicate.

If the result of compressing the file's content into the internal format is a duplicate, Git just re-uses the original again. Git updates its index copy with the right hash ID and it's done. If the result of compressing the file is all-new, Git arranges for the all-new data to get stored for real, and computes a new hash ID and puts that into Git's index.

In any case, the end result is that now, Git's index holds the updated, but still de-duplicated, file. So git add actually puts the file into Git—although it's not yet committed.

Note that if you remove a file with git rm, you remove it from both your working tree and Git's index. This is OK because the duplicate copy is still there in Git.

If you add a totally new file, Git will compress it down and see if its contents are a duplicate. If so, Git will add the new file's name and mode, with the hash ID for the duplicated contents. If not, Git will arrange for the new data to get stored, with a new hash ID, and will add the new file's name and mode, with the new hash ID. So this also works the same as adding an existing file, except that now the index has one more entry, for one additional file name.

The index—which Git also calls the staging area—is writable. It's the commits that are read-only. The index holds any updated files in a format that's ready to go into a new commit. It continues to hold any old files in their already-compressed, de-duplicated format, too. So the index, or staging area, always holds your next commit, ready to go.

Making a new commit

We're now ready to make our new commit, so we run:

git commit

Remember, we're in this situation to start:

...--G--H   <-- feature (HEAD), main

What git commit does is this:

  • It gathers any metadata it needs. That includes your name and email address, and the current date-and-time. Your name comes from the user.name setting and your email address comes from the user.email setting. This metadata also include any commit message you care to add, from -F file or -m message or using your editor on a temporary file or whatever.

    Crucially for Git itself, Git adds to this metadata the actual hash ID of the current commit, which it can find by reading the current branch name.

  • Now git commit packages up our snapshot, which is in Git's index. Whatever is in Git's index right now will be the snapshot in the new commit.

  • Git now writes all of this stuff out as a new commit. This new commit, because it is unique, gets a new, unique commit hash ID. We don't know what this hash ID will be—it depends on, among other things, the exact second that we make the new commit—but we'll just call it I.

  • Then there's one final trick, which we'll hold off for a moment.

Note that new commit I points back to existing commit H as its parent. So we'll draw this new commit, plus the old commits, like this:

...--G--H
         \
          I

The final trick here is that now that Git knows the new hash ID for new commit I, Git stores that hash ID into the current branch name. So now we have:

...--G--H   <-- main
         \
          I   <-- feature (HEAD)

Note that HEAD is still attached to feature. But feature now points to the new commit. The new commit points back to H as its parent commit, so when git log -p shows commit H as a set of changes, it will compare the files stored in I to the files stored in H.

Switching between commits

Now that you have this new commit I made, think about the current situation:

  • HEAD names feature, which names commit I.
  • Git made commit I from the files in Git's index (aka staging area), so I and the index match, in terms of snapshot.
  • You git add-ed all your changed files, so Git's index and your working tree match, in terms of file contents.

If you now run git checkout main, Git will:

  • Remove, from its index and your working tree, the files that go with commit I.
  • Insert, into its index and your working tree, the files that go with H.
  • Move the name HEAD to attach it to the name main.

You will now have:

...--G--H   <-- main (HEAD)
         \
          I   <-- feature

The files that you can see and work with—the ones in your working tree—are now those from commit H.

This is, to a large extent, what git checkout is about. Note that git checkout is smart: it won't switch if it can't, and has some special features that will sometimes let you switch branches even if you have uncommitted work. We won't cover all of this, but I'll mention that one of the smart tricks it has is: if the commit you're moving from and the commit you're moving to are the same commit, it never has to swap out any files, so it just doesn't. This means you can always do an "in place" switch as long as the two branch names—the current one, and the switch-to target—select the same commit.

We can now look at your sequence of commands:

  1. git checkout development

This will check out some existing branch named development, if you have one. That is, assuming that the current branch (whatever that is) points to some commit (whatever its hash ID is) and that Git's index and your working tree all match that commit, so that there's no problem in removing those files and going to the files for the commit selected by development, that's what Git will do:

...--G--H   <-- main
         \
          I--J   <-- development (HEAD)

for instance.

If you don't already have a branch named development, this would normally give you an error:

$ git checkout xyzzy
error: pathspec 'xyzzy' did not match any file(s) known to git

for instance. However, if there's a remote-tracking name development, Git will try to create a branch name from it.

We haven't covered remote-tracking names here yet, and I'm guessing—based on subsequent commands—that you actually ran:

git checkout -b development

instead. What that does is create a new branch name, pointing to the current commit, then select the new branch as the current branch:

...--G--H   <-- main, development (HEAD)

for instance.

My guess here may be wrong: your Git may well have created your development from their development; I'll cover that in just a moment.

  1. git push origin development

The git push command is a complicated one. It has your Git invoke—usually via https or ssh—another machine's Git software at some URL that will make that other machine's Git software refer to some other Git repository. Their Git software, working with their Git repository, prepares to receive new commits. I like to refer to their Git software and repository combination as "their Git" here.

Meanwhile, your Git software, working in your Git repository—"your Git", collectively—prepares to send them commits. Your Git now uses your branch name, development, to find commits. It sends, to their Git, the raw hash IDs of these commits, to see whether they already have those commits or not.

Their Git will tell your Git which commits they don't have, and therefore need, and which commits they do have, and therefore don't need. This part works entirely by hash ID, so your branch name's only function so far is to let your Git find your commits. Based on how you used git checkout in step 0, you won't have any new commits for them, so your Git will send no commits to their Git.

Having sent however many commits are required (none), your Git now asks their Git to set or update some of their branch names. This is where things get tricky. Their branch names are theirs. Your branch names are yours. The two need not match at all, and if you wanted, you could have run:

git push origin development:xyzzy

which would tell your Git to ask their Git to set their branch name xyzzy, rather than their branch name development.

Humans don't normally do this, because it gets too confusing. We like our branch names, in our repositories, to match the branch names in some other repositories that we're using as well. That helps us keep our own sanity. So you probably do want them to create a new branch name development.

If they do create a new branch name development at this point, that name will point to the same commit that your development points to. So they will have, in their repository:

...--G--H   <-- development

(their Git doesn't have this branch checked out, so their HEAD is not attached there).

Your Git now remembers their branch name for you. Your Git will create, in your repository, a remote-tracking name, origin/development. This name will remember which commit their development selects. So your repository now looks like this:

...--G--H   <-- main, development (HEAD), origin/development

Note that origin/development is not a branch name. In particular, Git cannot attach HEAD to that name.

If they already had a development

It's possible that their Git already had a development, and your Git knew that, so that you had, in your repository, something like this:

...--G--H   <-- main (HEAD)
         \
          I--J   <-- origin/development

In this case, if you run git checkout development and you do not have a development branch, your Git will scan for, and find, origin/development. The checkout code now invokes the --guess option—you can stop this with --no-guess—to figure out that you meant to run:

git checkout --track origin/development development

which means create for me a development branch pointing to the same commit as origin/development, and then check out my development. So that would result in this:

...--G--H   <-- main
         \
          I--J   <-- development (HEAD), origin/development

If this is what happened, your subsequent git push origin development would:

  • call up the Git at origin
  • list commit J for them, but they'd say "no need, I already have that one"
  • ask that they change their name development to point to (shared) commit J

and they'd say it already does, you are already up to date and do nothing.

So both methods wind up doing the same thing: they just select a different commit, based on whether you used git checkout -b, or git checkout --guess.

(There's one more difference here: with --guess, which is the default, your Git will set up your branch development with an upstream set, to origin/development. But this doesn't change any observable behavior here.)

Detached HEAD mode

  1. git checkout origin/development

You now have origin/development as a remote-tracking name, thanks to either something before step 0 (which then used the existing remote-tracking name) or to step 1 (which created it). When we talked about git checkout before, we were always giving it a branch name.

When you give git checkout a branch name, Git tries to check out that branch by attaching the special name HEAD to the branch name. But origin/development is a remote-tracking name, not a branch name. If you use the fancier new git switch command, it will give you an error here, but git checkout assumes that you know what you're doing and turns on the --detach flag for you.

With the --detach flag, Git will put you into detached HEAD mode. In this mode, the special name HEAD is no longer attached to any branch name at all. To draw that, we need something like this:

...--G--H   <-- main
         \
          I--J   <-- HEAD, development, origin/development

Commit J here is still the current commit, but Git obtains its hash ID directly from the name HEAD. The name HEAD is no longer attached to a branch name.

Since you're not changing commits, this is OK. Git changes nothing in its index, and nothing in your working tree. The only thing that happens is that you enter detached HEAD mode.

  1. remove few files

You don't mention how you remove them (with git rm or not). The removed files are removed from your working tree; with git rm, they're also removed from Git's index.

In any case, nothing has yet happened to any commit, and there are no new commits yet.

  1. git checkout development

You don't show running git commit. So at this point, your index doesn't match your current commit.

Fortunately (or unfortunately?), the commit you select here, which I'm drawing as commit J, is the same commit you're using right now. So Git will permit this git checkout, if that's really what you did at this point. Git will simply re-attach HEAD to the name development:

...--G--H   <-- main
         \
          I--J   <-- development (HEAD), origin/development

Your index and working tree remain undisturbed since Git did not have to change commits.

  1. modify same bunch of files

Given that you removed those files, it seems impossible for you to modify them at this point. They remain absent from your working tree. So your list of commands that you ran is clearly incorrect. One guess would be that you did run git commit before step 4, which would have made a new commit.

  1. git commit origin/development

The git commit command doesn't need origin/development as an argument, and often won't allow it. This likely would have produced an error message. The exception here is that git commit does allow you to specify particular files on the command line:

git commit fileA.txt fileC.txt

is short for:

git commit --only fileA.txt fileC.txt

The way this particular git commit operations is quite complicated, and it's usually a bad idea to use this form of the command. Note that origin/development would have to name a file in your working tree, which is why I expect it would have produced an error.

  1. git push origin development

As before, this will call up the Git over at origin and offer any new commits.

If you did make new commits at step 6, your Git will send over those new commits. Their Git will either accept those new commits, or not—GitHub in particular have some checks they do here to make sure there are no big files in the commits—and then move on to the update a branch name step.

Your Git will now ask them to set their development branch to match your development branch. If you get an error here, of the form:

 ! [rejected]        development -> development (non-fast-forward)

that error occurs because someone else got in and added new commits before you could.

Remember how we draw our commits:

...--G--H   <-- main
         \
          I--J   <-- development (HEAD), origin/development

This indicates that we have these particular commits, and that our name development finds commit J while our name main finds commit H. If we've made one or more new commits since then, we will have:

...--G--H   <-- main
         \
          I--J   <-- origin/development
              \
               K   <-- development (HEAD)

for instance. But suppose they, in their Git, received a new commit or two and *added those commits to their development, so that they have:

...--G--H   <-- main (HEAD)
         \
          I--J--L   <-- development

When we send them our K, they will wind up with this:

...--G--H   <-- main (HEAD)
         \
          I--J--L   <-- development
              \
               K   <-- [just received]

We then ask them to set their development to point to new commit K. They could obey this request, but if they do, they will have no way to find commit L any more. They will have, in their repository:

...--G--H   <-- main (HEAD)
         \
          I--J--K   <-- development
              \
               L   [abandoned]

The default response to this kind of request to "abandon" some set of commits is to reject the request.

For more details, see Git push rejected "non-fast-forward".

torek
  • 448,244
  • 59
  • 642
  • 775