On Mon, Dec 04, 2017 at 06:51:42AM -0500, Nick Sabalausky (Abscissa) via Digitalmars-d-learn wrote: > On 12/03/2017 03:05 PM, bitwise wrote: > > I've finally started learning git, due to our team expanding beyond > > one person - awesome, right? > > PROTIP: Version control systems (no matter whether you use git, > subversion, or whatever), are VERY helpful on single-person projects, > too! Highly recommended! (Or even any time you have a directory tree > where you might want to enable undo/redo/magic-time-machine on!)
+100! (and by '!' I mean 'factorial'. :-P) I've been using version control for all my personal projects, and I cannot tell you how many times it has saved me from my own stupidity (i.e., have to rollback a whole bunch of changes, or just plain ole consult an older version of the code that I've forgotten). Esp. with git, it also lets me play with experimental code changes without ever worrying that if things don't work out I might have to revert everything by hand (not fun! and very error-prone). In fact, I use version control for more than just code: *anything* that's text-based is highly recommended to be put under version control if you're doing any serious amount of editing with it, because it's just such a life-saver. Of course, git works with binaries too, but diffing and such become a lot easier if everything is text-based. This is why I always prefer text-based file formats when it comes to authoring. Websites are a good example that really ought to be under version control. Git, especially, lets you clone the website to a testing server where you can experiment with changes without fear, and once you're happy with the changes, commit and push to the "real" web server. Notice an embarrassing mistake that isn't easy to fix? No problem, just git checkout HEAD^, and that buys you the time you need to fix the problem locally, then re-push. I've also recently started putting certain subdirectories under /etc in git. Another life-saver when you screw up a configuration accidentally and need to revert to the last-known good config. Also good for troubleshooting to see exactly what changes were made that led to the current state of things. tl;dr: use version control WHEREVER you can, even for personal 1-man projects, not only for code, but for *everything* that involves a lot of changes over time. > > Anyways, I've got things more or less figured out, which is nice, > > because being clueless about git is a big blocker for me trying to > > do any real work on dmd/phobos/druntime. As far as working on a > > single master branch works, I can commit, rebase, merge, squash, > > push, reset, etc, like the best of em. > > Congrats! Like Arun mentioned, git's CLI can be a royal mess. I've > heard it be compared to driving a car by crawling under the hood and > pulling on wires - and I agree. > > But it's VERY helpful stuff to know, and the closer you get to > understanding it inside and out, the better off you are. (And I admit, > I still have a long ways to go myself.) Here's the thing: in order to use git effectively, you have to forget all the traditional notions of version control. Yes, git does use many of the common VC terminology, and, on the surface, does work in similar ways. BUT. You will never be able to avoid problems and unexpected behaviours unless you forget all the traditional VC notions, and begin to think in terms of GRAPHS. Because that's what git is: a system for managing a graph. To be precise, a directed acyclic graph (DAG). Roughly speaking, a git repo is just a graph (a DAG) of commits, objects, and refs. Objects are the stuff you're tracking, like files and stuff. Commits are sets of files (objects) that are considered to be part of a changeset. Refs are just pointers to certain nodes in the graph. A git 'branch' is nothing but a pointer to some node in the DAG. In git, a 'branch' in the traditional sense is not a first-class entity; what git calls a "branch" is nothing but a node pointer. The traditional "branch" is merely a particular configuration of nodes in the DAG that has no special significance to git. Git maintains a notion of the 'current branch', i.e., which pointer will serve as the location where new nodes will be added to the DAG. By default, this is the 'master' branch (i.e., a pointer named 'master' pointing to some node in the DAG). When you run `git commit`, what you're doing is creating a new node in the DAG, with the parent pointer set to the current branch pointer. So if the current branch is 'master', and it's pointing to the node with SHA hash 012345, then `git commit` will create a new node with its parent pointer set to 012345. After this node is added to the graph, the current pointer, 'master', is updated to point to the new node. By performing a series of `git commit`s, what you end up with is a linear chain of nodes, with the current branch ('master') pointing to the last node. This, we traditionally view as a "branch", but in git, there is nothing special at all about this chain; it's just a (sub)graph of some nodes. The git 'branch' is nothing but a pointer to the last of these nodes. You can easily make this pointer point to something else -- you wouldn't normally do this, but sometimes it can be useful. You can also decide that instead of adding new nodes to 'master', you want to add new nodes elsewhere in the DAG. No problem, just `git checkout` some arbitrary node, and start running `git commit` on it. The first new commit will take that node as parent, and thereby start creating a new chain of nodes "branching off" the 'master' chain. Merging a branch in git is likewise not something you'd think of in traditional VC terms; it's basically nothing but creating a new node with two parents, one from the tip of each respective branch. You can 'merge' any two arbitrary nodes together. Though of course, in general you'll end up with a huge number of conflicts if the node contents aren't correlated with each other -- but git doesn't actually mind that; you can actually overwrite all the contents with something else altogether and commit that, and git will happily take that as the "merge" of the two unrelated branches. The resulting graph won't make any sense in terms of revision history in the traditional VC sense, but git doesn't care. The point is that as far as git is concerned, it's all just a DAG. The fact that the contents of two adjacent nodes happen to be similar is just a "coincidence", albeit a usual one. The more 'arcane' git operations like rebasing, history rewriting, etc., are at the end of the day nothing more than graph operations, updating a bunch of pointers and moving nodes around. If you begin thinking of your repo as a graph and forget traditional VC notions of branches, you'll find that git suddenly starts to "makes sense", and you'll be able to do amazing things to your repo without losing your way. [...] > ([...] there's nothing worse than accidentally loosing a bunch of > important code, or finding you need to undo a bunch of changes that > didn't work out.) If you think in terms of graphs, you'll hardly ever need to worry about losing changes. Just think in terms of code: if you were given a bunch of pointers to nodes in a graph, and you need to update these pointers, what's the safest way to do it? Easy: just save the pointers to some local variables, then do whatever updates you want, and if it doesn't work out, just overwrite the pointers with the saved values, and you're back to where you started. In git, because everything is SHA-hashed, nodes are actually immutable. Even the so-called history rewriting, technically speaking, isn't really "rewriting"; it's actually creating a NEW subgraph that just happens to be similar to the older part of the graph plus some changes, and updating your refs (pointers) to point to nodes in the new part of the graph instead. In git, nodes that have nothing pointing to them are considered garbage; `git gc` will delete them from the graph. So once all your pointers are pointing to the new nodes, you've effectively discarded the old nodes; hence the overall effect is "rewriting" the graph. But if you still keep a ref to the old nodes, they will still be there; nothing is be lost. It's like dealing with immutable values in D: you can never change them, but you *can* make (modified) copies of them and changing your pointers to point to the copies instead of the original values. As long as you still keep refs to the old nodes, they will never be lost no matter what you do to your graph. And note that the parent pointers in each node are also part of the SHA hash, so the topology of the old part of the graph is immutable too. There is literally nothing you can do that can change the content or topology of those old nodes. As long as you have a way to reach them, you will still have your old history completely intact. And how do you create backup copies of your pointers? Easy: remember a git 'branch' is nothing but a pointer? Well, so you just go `git checkout <branch>; git checkout -b backup_ref` and now you have a pointer called 'backup_ref' that points to that same node that <branch> is pointing to. Now you can do whatever you want to <branch> -- add new commits, overwrite it with a ref to a completely different node, whatever. If at any point you decide that you want it to point to the original node again, just `git checkout <branch>; git reset --hard backup_ref`. As long as you don't touch backup_ref, you will be able to go back to the original state. (See? This is why you have to stop thinking of a git repo in traditional VC terms. Your git repo is a graph. (With immutable nodes.) That's all there is to it.) > One thing to keep in mind: Any time you're talking about moving > anything from one repo to another, there's exactly two basic > primitives there: push and pull. Both of them are basically the same > simple thing: All they're about is copying the latest new commits (or > tags) from WW branch on XX repo, to YY branch on ZZ repo. All other > git commands that move anything bewteen repos start out with this > basic "push" or "pull" primitive. (Engh, technically "fetch" is even > more of a primitive than those, but I find it more helpful to think in > terms of "push/pull" for the most typical daily tasks.) Again, this will all make so much more sense if you think in terms of graphs. What `git fetch` does is to download a bunch of nodes from a remote source. Don't even think in terms of branches; think in terms of individual nodes (which imply their own graph connectivity structure -- because the parent pointers are an immutable part of them) that are downloaded from the remote source. After downloading these nodes, git will create a new pointer (i.e., ref) to point to the last node (i.e., the node from which the other nodes can be reached), usually with a name like upstream/somebranch. There is nothing special about this name besides the convention that we use names of the form x/y for pointers named 'y' that we downloaded from 'x'; it's just a pointer to some nodes that you downloaded off the 'net. What 'git pull' does is to try to reconcile these downloaded nodes with the nodes in your local branch -- and here is where wrinkles can arise, because, by convention, git will try to merge the nodes from x/y into the local branch called y. It's all good if the local branch y points to an ancestor of x/y, i.e., your local branch is just a subgraph of the remote branch, and since the parent pointers of the downloaded nodes already point to y (i.e., they are already a part of the graph! -- because they share an ancestor node), the only thing that's needed is to update y to point to x/y (i.e., the new tip of the branch) instead. This is called 'fast-forwarding'. But what if your local branch has diverged from the remote branch? I.e., the nodes in local branch 'y' share a common ancestor with the downloaded nodes in x/y, but have different descendent nodes. Now we cannot simply set y to x/y, because that would cause you to lose your pointer to your local nodes, which means `git gc` will garbage-collect them (i.e., your local changes will be lost). So git tries to be 'helpful' here by attempting to merge the nodes together -- i.e., create a new series of nodes that incorporate the changes from *both* y and x/y. Unfortunately, this process often causes further problems, because remember, nodes are immutable, so the only way you can merge the changesets together is by creating new nodes ("merge commits" in git parlance) and discarding the old ones. But once you do that, your local branch 'y' is no longer the same as the remote one, so when it comes time to push your changes to other collaborators, or to pull from remote again later, it causes more conflicts in a never-ending spiral. The best approach is to avoid this situation altogether, by designating certain branches (usually master) as pull-only, i.e., you never commit changes to them, all your changes are committed to local branches. In terms of graphs, you never change the value of the 'master' pointer, but may add new nodes to the graph by using other pointers ("local branches") for that purpose. Then `git pull` will always be fast-forward only (the value of the local 'master' pointer will always be equal to, or an ancestor of, the remote 'master' pointer, so it is always possible to just replace the local 'master' pointer with the remote value without losing any nodes). This is why I recommend to *always* run: git pull --ff-only upstream master The --ff-only tells git not to try to be smart and create a mess of merge commits, but to only ever fast-forward the master pointer. If this fails, then you know you've made a mistake and updated the master pointer where you should have used a local branch instead. (How to fix this is left as an exercise for the reader: hint, remember 'master' is just a pointer. Just create a new local branch to point to the current nodes, i.e., backup your pointer, then reset 'master' to the last common ancestor with the upstream nodes, then `git pull`, and rebase your local branch afterwards.) > > How does one keep their fork up to date? For example, if I fork dmd, > > and wait a month, do I just fetch using dmd's master as a remote, > > and then rebase? If you keep to the convention of never committing to master locally, then you can just `git pull --ff-only upstream master` and it will pull in the latest changes. Then you just rebase your local branch(es) on top of master. In graph-centric terms, running `git rebase master` in a local branch B does the following: (1) find the common ancestor A of master and B; (2) for each node in B up to (but not including) A, create a corresponding new node that contains the same changes, but is based on the tip of master instead of A; (3) set B to point to the last of the new nodes. Special note: since rebase isn't actually modifying nodes -- remember nodes are immutable -- if you're unsure or want to be extra-careful, you can keep a spare reference to the old tip of B before running the rebase, like this: git checkout B git checkout -b B-backup # backup pointer git checkout B # set current branch back to B git rebase master # rebase B onto master If you then run `git log --graph --all`, you'll see that there are now *two* copies of the commits you made in B: one in the original position branching off master at ancestor A, and the other is now based on master. 'B' will now point to the new nodes, but you'll still be able to access the old nodes via 'B-backup'. If at any time you wish to 'undo' the rebase, just reset B to B-backup. (The new nodes will then become unreferenced, and will be garbage-collected. Unless you kept another pointer to them, of course.) See? No danger of data loss. (Unless you forget to keep a spare pointer to your old nodes. But even in that case, there's still a way out with `git reflog` -- git gc doesn't actually delete nodes until they're past a certain age, so as long as you notice the problem early and not a week or month later, your old nodes will still be there. You just have to dig through `git reflog` to find the old pointer values, i.e., SHA hashes. Once you find the right SHA hash, just `git checkout <hash>` to go back to the old node, then `git checkout -b <oldbranch>` to create a new branch pointer to point to the old nodes.) [...] > > and do I need a separate branch for each pull request, or is the > > pull request itself somehow isolated from my changes? > > You *should* create a separate branch for each pull request unless > you're a masochist. There's *no* isolation other than whatever > isolation YOU create. (Not my idea of award-winning software design, > but meh, it is what it is). > > This is why people are adamant about making a separate branch for each > pull request. *Technically* speaking you don't absolutely HAVE > to...But if you *don't* create a separate branch for each PR, you're > just asking for pain: It'll be a PITA if you want to create another PR > before your first one is approved and merged. And it'll be a PITA if > your PR is rejected and you want to do any more work on the codebase. [...] Just think of it as updating a graph. You have a local copy of the graph, and you've added a bunch of new nodes to it. Now you want the upstream people to add your new nodes to their copies of the graph too. Suppose further that these nodes represent several different changesets. What's the best way to manage these nodes? It should be obvious that the best way is to use a different pointer for each changeset, so that if the upstream people decide to merge changeset A but reject changeset B, you can keep your local copy of the graph straight. If you use the *same* pointer for all changesets, then it should be no surprise when things become a big mess when upstream merges some changesets but not others, yet locally you have no way of addressing each changeset separately. Even if all your changes eventually get merged, in the interim you may be running git rebase to apply your changes to the latest upstream code; if you only keep a single pointer around for everything, you're going to lose track of what's going on really quickly. There's no *requirement* that you do things this way, of course, but it's just a matter of being able to keep your own changesets straight when you have to reconcile your local graph with the remote one. T -- Never wrestle a pig. You both get covered in mud, and the pig likes it.