0

A partner and I are working on a project and I cloned from her branch to use what she had as my starting off point. Well the project is tied to her branch and I am trying to find a way to untie it. I am new to github and would like to know of any way that I can push the changes I made to my own branch in the same repository. Thanks for any help.

SieSie
  • 1
  • 2
  • you can research more on [git remote](https://git-scm.com/book/en/v2/Git-Basics-Working-with-Remotes) – AlexWei Dec 06 '20 at 16:06

1 Answers1

2

... and I cloned from her branch

Actually, you didn't. That's simply not possible. When you use git clone, you clone a repository, and the only from involved is a URL. You clone from a URL, and in general, you clone the entire repository.1 This means your next job is easy, or at least, easier.


1The exceptions to these rules involve using --depth for a shallow clone, using --single-branch—note that --depth implies this flag too—and/or using the not yet ready for general-use --filter option to produce a partial clone. If you did not use any of these special options, you got a full clone.


... the project is not tied to her branch and I am trying to find a way to untie it.

In Git, branches are not important. They're useful—branch names are how you find commits—but they are not important, in that you can change them any time, add new names, delete old names, and so on. What matters are commits. We'll get back to this in a moment.

I am new to github

Except when you are using the web interface to GitHub, the presence of GitHub in this is all irrelevant. You will do your work on your own computer, using your everyday computer tools, and the tools provided by the Git system (which add on to your everyday computer tools).

and would like to know of any way that I can push the changes I made to my own branch in the same repository.

This part is trivial, but you might want to learn something less trivial: you don't really have changes, for instance. You have commits. Those commits store files—particular versions of files, stored for all time2 in the form they had at the time you made those commits.


2Well, stored for as long as the commits themselves continue to exist anywhere.


Git is all about commits

Those new to Git, like yourself, often think Git is about branches (as you did). But it's not: it's about commits. Branches—or rather, branch names—just help you (and Git) find commits. Others often think Git is about files. It's not about those either. Commits contain files, because files are important to humans, to get their work done. But Git is about the commits.

Every commit in Git has a number. This number is big, ugly, and random-looking—though not actually random at all—and expressed as, e.g., faefdd61ec7c7f6f3c8c9907891465ac9a2a1475. The number is unique to that one specific commit and once assigned to a commit, can never be used for any other commit. (That's why it has to be such a big number.) This way, any two Git programs, looking at any two Git repositories, can talk to each other and figure out whether one repository has the same commit as some other repository, just by exchanging the numbers.

With that in mind, we can now say that a repository is, in essence, just two databases. The primary, and usually much bigger, database is a simple key-value store that holds internal Git objects, such as commits.3 The key is the number and the value is the internal Git object: the commit itself, in the case of an internal commit object.

(All Git objects—hence all commits—are read-only. The reason is that the hash ID is simply a cryptographic checksum of the contents of the object. If you copy one of these objects out of the database (e.g., extract a commit) and modify it and put it back in, what happens is that you add a new object with a new unique hash ID. The existing objects continue to exist. The only way not to add a new object is to exactly duplicate an existing one.)

What's in a commit is a little more complicated, but we can summarize it this way. There are two parts to each commit:

  • A commit holds data, which is a snapshot of all the files that Git knew about at the time you, or whoever, made the commit. It's a complete snapshot, not a set of changes. The files' contents are stored internally as blob objects, which automatically de-duplicates them, so it's cheap to store all the files all over again: Git just re-uses the old object.

  • A commit also holds metadata, which is information about the commit itself. This includes the committer's name and email address, and a date-and-time-stamp for when they made that commit. It includes any log message they want to add to the commit. And, crucially for Git itself, it includes the hash ID(s) of some set of earlier commits.

This last part—a commit storing the hash ID of what Git calls its parent commit—is how history works. Suppose we have a simple linear chain of commits, each with a unique hash ID. Let's draw their hash IDs as single uppercase letters, because real hash IDs are too big and ugly to deal with. We end up with this:

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

where H stands in for the hash ID of the last commit in the chain.

Using the hash ID of commit H, Git can get at a full snapshot of all the files as of the form they had when you made commit H. But it can also get the metadata, which includes the hash ID of earlier commit G. We say that commit H points to earlier commit G.

Moving backwards one step, then, Git can now use the commit hash ID of G to get G: the snapshot of all files, and the metadata. Git can compare the snapshot in G against the snapshot in H to see what changed, if you like. But Git can also use the metadata from G to find the hash ID of still-earlier commit F.

Since each commit points backwards one step, all we need, to find all the commits in the chain—all the way back in history to the very first commit ever—is the hash ID of the last commit in the chain.


3Internally, there are four kinds of Git objects; all get these hash ID numbers, and all are stored in this database.


Branch names find commit hash IDs

To make use of the chain above, we need to know the hash ID of the last commit in the chain, the one we called H. We could write this hash ID down, or memorize it. But that would be silly: we have a computer. Let's just have the computer remember it, perhaps in a file. We could name the file feature or branch1 for instance.

This is the function of a branch name. Each branch name simply holds the hash ID of one commit: the last one in some chain. Suppose we have this chain:

...--F--G
         \
          H

where H is the last one, and G is also a last one because H is in a different branch. We just need names to point to commits G and H, like so:

...--F--G   <-- main
         \
          H   <-- develop

Here, develop finds a new commit, added since the last stable commit, which we find with the name main.

What Git really needs are the commits. We need to know that commit G is the last one on main, so we need the name main to point to G. We need to know that H is the last commit on develop, so we need the name develop to point to H. But Git really only needs the commits.

Picking a branch name to use, and getting the files out of a commit

The files in a commit are all read-only. They are also in a form that only Git itself can read (internal objects are not usable by most non-Git programs). They literally cannot be used for new development. If we want to add a new commit to develop, we have to get Git to copy the files out of commit H and turn them into regular everyday files. We also need to have Git remember that the commit we did this with was commit H.

The way we do this is with git checkout or (since Git 2.23) git switch. We pick some branch name to use and have Git get "on" that branch. The way Git remembers the branch name is that it attaches the special name HEAD to the branch name:

...--F--G   <-- main
         \
          H   <-- develop (HEAD)

It also copies all the files out of the commit at this time. The copied-out files go into your working tree or work-tree, where you can see them and work on/with them. These files are not actually in the Git repository at all. They were copied out of the repository and you can get work done now, but if you do change the files, you will, later, need to have Git make a new snapshot.

You already know how to do this, using git add and git commit. You may or may not know about Git's index, which Git also calls its staging area; we won't go into any details here except to mention that git commit actually uses the copies from the index / staging-area, not the copies from your working tree.

When you do urn git commit, Git packages up the files it knows about (because they're in its index / staging-area). Git adds the appropriate metadata, including your name and email address and the current date-and-time as set in your computer's clock. Git adds the hash ID of the current commit, and uses all of this stuff to create a new commit:

...--F--G   <-- main
         \
          H--I

As soon as Git finishes writing out commit I—which assigns the commit its new unique hash ID4—Git writes that hash ID into the name to which HEAD is attached, giving us this:

...--F--G   <-- main
         \
          H--I   <-- develop (HEAD)

Your HEAD is still attached to your branch name develop, and your branch name develop now selects commit I.


4Note that the hash ID depends on the exact second at which you make the commit. The date-and-time-stamp is part of the metadata, and the cryptographic checksum includes that date-and-time-stamp. This means that there's no way to predict, in advance, what hash ID some commit will get. You just have to make the commit; that's what gives the commit its hash ID.


Cloning a repository

Before we go on to the next step, let's take a look at how git clone really works. This also involves the git fetch command, so it's a pretty big step.

The clone command is really a convenience command. It wraps up a series of six commands all into one. Five of these six commands are Git commands. The first one is just your own computer's "make a new directory / folder" command:

  1. mkdir (or whatever your computer's command is): this makes a new, empty directory. When you run git clone url, Git takes the last part of the url and turns that into the directory name. For instance, git clone https://github.com/git/git clones a Git repository for Git itself. The last part of this URL is git so your own Git will make a new empty git directory.

(The remaining five commands are all run in the new directory, though when this is all done, you're still not in the new directory. You will have to move your command interpreter into that directory with, e.g., cd git, or whatever your command line interpreter uses.)

  1. git init: this creates a new empty repository in the otherwise empty directory. The repository goes in a sub-directory named .git.

  2. git remote add origin url: this adds a remote named origin to this new empty repository. A remote is mainly a short name for a URL, so that Git can expand origin to this URL again later. However, the name origin is also used in step 5. (You can pick a different name, other than origin, here, if you like, but there's usually no reason to choose something else.)

  3. The clone command now runs any git config commands required by command line options you gave to git clone. If you didn't use any extra options, step 3 here doesn't actually do anything; I include it only for completeness, to show that the configurations happen before step 5.

  4. git fetch origin: this is the most complicated part of the operation. This step fetches all the commits from the other Git repository, but none of the branches.5 The fetch command works by examining their branch names and corresponding commit hash IDs, and using this information to get any commits they have, that you don't. This is (normally—see footnote 5) all of their commits. So this step gets you all their commits. But git fetch then takes their branch names and changes them. It makes them into what Git calls remote-tracking names, which aren't branch names.6 Their main becomes your origin/main; their feature/tall becomes your origin/feature/tall; their develop becomes your origin/develop, and so on.

  5. git checkout branch: this step creates one new branch in your repository. Note that your branches are yours, not anyone else's. Their branches became your remote-tracking names.

The new branch your Git creates here is your choice: it's from the -b option you gave to git clone. If you did not give a -b option to git clone, your Git asks their Git what branch name they recommend, and uses that name here. This still creates your own branch name.

The end result of all of this is that before the last step, your own repository has, e.g.:

...--F--G   <-- origin/main
         \
          H--I   <-- origin/develop

The special name HEAD can't be attached to a remote-tracking name (which is half of why they're not branch names). The last step, to run git checkout, creates a new branch name. It's spelled the same as one of these origin/* names, minus the origin/ part, which means it's spelled the same as one of the other Git's branch names; and it points to the same commit as one of those names. Let's say you picked develop as the name for your Git to create. Then you now have:

...--F--G   <-- origin/main
         \
          H--I   <-- develop (HEAD), origin/develop

in your own repository, with commit I checked out.


5There are some special cases where this isn't quite right, including of course the --depth and --single-branch ones I mentioned at the top. The fetch operation can be limited. It can also be made to copy branch names, rather than renaming branches. But these are unusual modes of operation, and we won't go into them here.

6Git actually calls them remote-tracking branch names. But they're still not branch names, and the word branch in this is just a distraction. I recommend leaving it out.


With all this in mind, we can get back to your question

[my] project is not tied to her branch and I am trying to find a way to untie it.

You should now look at your repository and find which commits you have, and which branch and remote-tracking names find those commits. Consider using git log --all --decorate --oneline --graph or any other Pretty git branch graphs viewer.

Now, suppose you have made new commits that follow someone else's commits:

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

To make your changes into new commits, first you're going to have to extract changes. Commits hold snapshots, not changes. But Git can easily find changes, because any two adjacent snapshots can be compared, like a simple game of spot the difference. Git has a difference-spotter built in.

We need a new name so that Git will remember new hash IDs for us

Your existing commits J and K, in this illustration, literally can't be changed. That's not a problem, just something to keep in mind.

Let's first create a new branch name and make it point to existing commit G, just like your origin/main points to commit G:

git branch newname <hash-of-commit-G>

or:

git branch --no-track newname origin/main

These two commands result in this:

...--G   <-- newname, origin/main
      \
       H--I   <-- origin/develop
           \
            J--K   <-- develop (HEAD)

We then need to run git checkout newname to move HEAD to the name newname, and get commit G checked out:

...--G   <-- newname (HEAD), origin/main
      \
       H--I   <-- origin/develop
           \
            J--K   <-- develop

There's a reason we added --no-track to the command that used origin/main. It's not really very important and we don't actually need to do that at all, and this flag is a bit confusing, so I'm not going to bother explaining it either. There's a different way to create the name newname, using two Git commands:

git checkout main
git checkout -b newname

This has a slightly different effect and is itself shorthand for:

git branch main origin/main
git checkout main
git branch newname
git checkout newname

The first step creates a new branch name main in your own repository, using the name origin/main as the commit-finder, so that main points to commit G. Since we didn't add --no-track, the new name main has origin/main set as its upstream.7 This is true whether we use git checkout -b or git branch to make the name. If we use git branch to make the name main, we then use git checkout to get on the new branch, so that Git extracts commit G for us to work with. Either way we end up with the special name HEAD attached to the new name main:

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

The second step, git checkout -b newname or the two commands git branch newname and git checkout newname, makes a new branch name, newname, pointing to the current commit (G, still) and then attaches HEAD to that name:

...--G   <-- main, newname (HEAD), origin/main
      \
       H--I   <-- origin/develop
           \
            J--K   <-- develop

The difference here is that we created a new name main to identify commit G, before creating the new branch name newname to also identify commit G. We don't actually need the branch name main at all: the remote-tracking name origin/main works just as well for our purposes. It's up to you whether to bother to create the name main.

Note: it's perfectly fine to create the branch name main now, and then, tomorrow, if you decide you didn't want it, to simply delete the branch name main. You can add and delete branch names any time you like! The only thing to remember is that the name is an easy way to find any one particular commit, so if you want to find that one commit easily, you might want to keep that name. The point of a branch name is to let you, and Git, find a commit—and to let you get "on" that branch and make new commits that automatically update the branch name. If you don't need an automatically-updated name, you don't need a branch name, but you can still use one any time you feel like it.


7The word upstream was developed later, after the --track and --no-track options. These use the word "track", which is badly overloaded in Git. The word "upstream" is a better word here, but for now we're stuck with --track and --no-track as the option spellings. You don't need to remember all of this.


Now that we have our new branch name, let's copy the commits

To make our new branch independent, yet still have some new commits on it, let's copy the existing J-K commits. Remember, we're assuming at this point that we have this picture:

...--G   <-- newname (HEAD), origin/main
      \
       H--I   <-- origin/develop
           \
            J--K   <-- develop

That means there are two commits that are "on" our own develop that we'd like to copy. We don't want to copy commits H and I, only J and K need copying.

The command to copy a commit—that is, turn a commit into differences from its parent commit, and then apply those differences to our current commit and make a new commit from that—is git cherry-pick. We can do this one commit at a time:

git commit <hash-of-commit-J>

for instance. To get the hash of commit J, we can use git log or one of those fancier pretty-commit-graph viewers to find the hash ID for commit J, then just cut-and-paste it. We'll see an easier way in a moment, but let's just imagine we did that.8 We get this as the result:

       J'  <-- newname (HEAD)
      /
...--G   <-- origin/main
      \
       H--I   <-- origin/develop
           \
            J--K   <-- develop

The cherry-pick operation has copied commit J, to a new-and-improved version J'. The change from G to J', in the copy, will be the same as the change from I to J', in the original. The metadata will mostly be copied, except for some updated timestamps and that the parent of new commit J'is existing commitG, not commit I`.

We then repeat this for commit K, producing:

       J'-K'  <-- newname (HEAD)
      /
...--G   <-- origin/main
      \
       H--I   <-- origin/develop
           \
            J--K   <-- develop

We now no longer need the name develop at all. If we like, we can delete it entirely (git branch -D develop). There's one slight danger, and that's that if we do delete it, we can't find the actual hash IDs of commits J and K in the future. If we decide we want original-J and original-K again, we should perhaps keep the name around (or add another name instead of develop, or rename develop to some other name, or whatever).


8Or, better yet, you can actually try this out.

I hear and I forget.
I see and I remember.
I do, and I learn.

      —possibly, but probably not, a distorted Chinese proverb

If you do try it out, you might want to do it in another clone. You can clone your own clone. Just remember that your branch names will become remote-tracking names in the new clone, when you do this; your existing remote-tracking names in the existing repository won't get copied at all into the clone.


An easier way: en-masse cherry-pick

Rather than manually finding the hash IDs of commits J and K, we can just have git cherry-pick itself do the work, by running:

git cherry-pick origin/develop..develop

This form, with the two dots in it, is how we express, in general, to Git, the idea of finding a range of commits. I won't go into any detail in this answer, which is already pretty long.

Another easier way: rebase

I won't cover this one at all here, but git rebase consists of doing an en-masse cherry-pick operation followed by moving a branch name. If we let Git do this with our existing name develop, then rename develop, we can get the effect of copying the commits, then deleting the old name develop. (To make it exactly the same, we also need to unset the upstream. The rebase needs the --onto flag to be able to do the right thing.)

Once we're done, we just git push our new branch

Without going into the details of how git push really works, once we have:

       J'-K'  <-- newname (HEAD)
      /
...--G   <-- origin/main
      \
       H--I   <-- origin/develop
           \
            J--K   <-- develop

we can just run git push -u origin newname, assuming origin is the name for the URL of the GitHub repository. This has our Git call up their Git and send to them our commits J' and K', then ask them to create or update their name newname to remember commit K'.

Since they won't have a branch name newname yet, this will create a branch named newname. They will then have, in their repository:

       J'-K'  <-- newname
      /
...--G   <-- main
      \
       H--I   <-- develop

Note how their names do not begin with origin/. They have the same commits—commit hash IDs are universal across all Git repositories, everywhere—but different branch names.

torek
  • 448,244
  • 59
  • 642
  • 775