Exposing my Ignorance

by Aaron Wells
Tags: profile

A blog where I talk about things that are new to me.


My git cheat sheet

There's an XCKD comic making the rounds, which sums up a lot of peoples' experience with git:


I hear this a lot, and indeed, it's sorta built into our own Mahara developer documentation. Git has an awfully steep learning curve.

But personally, I've never had much of a problem with git. Before I started at Catalyst, I had used CVS and Subversion. I remembered that when I'd switched from CVS to Subversion, all my old CVS knowledge was mostly useless and I'd had to actually read up on how Subversion works in order to avoid problems with it. So, I did the same thing when I switched to git. I spent one or two days in my first week reading the free online Pro Git book, until I understood the concepts behind it and what the commands were doing. I've never really been confused or had any problems since then.

... or have I? As I tweeted a self-congratulatory tweet about my git mastery, I remembered I actually keep an extensive "git cheat sheet" in the notes program on my computer. I consult it frequently, not so much because I find git confusing, but because I'm not great at remembering the specific syntax for commands. I keep similar cheat sheets for a lot of things; a quick search shows 19 of them at present.

Anyway, I thought my git cheat sheet might be handy to someone else, so, in the spirit of exposing my ignorance, here it is. The notes in here have been accrued over the course of nearly 6 years, some ranging from abstract information when I first learned git which I have seldom used since, and some I consult on weekly basis. Mostly the newest stuff is at the top, and the oldest stuff is at the bottom.

Git cheat sheet

Do git diff in meld (or other graphical tool)

git difftool

Uses exactly the same arguments as "git diff", but it displays the diff in the tool of your choice instead of the terminal.

Great for when you really want to just diff one function in lib/view.php.

Undo nearly anything


Accessing the weird gerrit meta/config refs


git fetch gerrit refs/meta/config:refs/remotes/gerrit/meta/config
git checkout meta/config
... make changes
git push gerrit meta/config:meta/config

... gerrit itself gives basically the same suggestion:

git fetch origin refs/meta/config:config
git checkout config
git push origin HEAD:refs/meta/config

    label-Automated-Tests = -1..+1 group Mahara Automated Testers
[label "Automated-Tests"]
    abbreviation = T
    rule = NoBlock
    value = -1 Tests failed
    value = 0 Tests not run
    value = 1 Tests passed

List all tags in a remote

git ls-remote --tags

Delete a remote tag

git tag -d
git push :refs/tags/

How to push a tag
You can push all them with:

    git push --tags

... but in some setups I've used, they've got a stupid thing set up that sends out an email for each tag, and they may complain if you push all of them at once. But it's super easy to just push one tag:

git push repo tagname

Bammo. If you have a tag and a branch with the same name, you can be more specific:

git push repo refs/tags/tagname

And to delete a tag

git push repo :refs/tags/tagname

Cherry-pick --theirs

git cherry-pick --strategy=recursive -Xtheirs

Search all git branches

git log --all

View an earlier version of a file without checking it out

git show REVISION:path/to/file

Make an existing branch track an upstream branch

git checkout localbranch
git branch --set-upstream-to remote/branchname

OR when pushing it, do
git push -u

Do a merge of two branches that were parallel for a long time

This is what I did when merging Moodle 2.2 into Massey's Stream at Moodle version 2.1. It greatly reduced conflicts and problems, but it doesn't really square with my theoretical understanding of how git merges actually work. This is taken from the Moodle forums: http://moodle.org/mod/forum/discuss.php?d=191774#p834646

git checkout -b merge_helper_branch MOODLE_[later]_STABLE
git merge --strategy=ours MOODLE_[earlier]_STABLE
git checkout -b my_custom_[later] my_custom_[earlier]
git merge --strategy-option=patience merge_helper_branch

Recover lost git commits

There are plenty of guides to this online. Here's one: http://feeding.cloud.geek.nz/2008/12/recovering-lost-git-commits.html

Have multiple working directories sharing the same git repo

git-new-workdir /path/to/repo /path/to/new/workingdir

Useful because I like to have multiple git instances accessible simultaneously on my computer at different locations under /var/www. This way, I only have to have one actual git repo that I keep up to date.

Check out a new remote tracking branch

Usually it's good enough to just do a git checkout of a new branch that has the same name as exactly one remote branch, and then it will automatically set it up for you (apparently this is controlled by the flag branch.autosetupmerge).

But if that doesn't work, this should:

git checkout -t remoteName/remoteBranchName

(The reason the syntax is like this, it's because "-t" works on its own, and "remoteName/remoteBranchName" is the starting point commit, and the new branch name is automatically generated)

Name of current branch's tracking remote

git config branch.`git name-rev --name-only HEAD`.remote

More generally...
LOCAL_BRANCH=`git name-rev --name-only HEAD`
TRACKING_BRANCH=`git config branch.$LOCAL_BRANCH.merge`
TRACKING_REMOTE=`git config branch.$LOCAL_BRANCH.remote`
REMOTE_URL=`git config remote.$TRACKING_REMOTE.url`

Minimize the size of a repository

This is the process I do when I'm bringing code down to a locked-down client on a USB stick, and I only need to bring info about two branches, and I need to upload the repo over a slow connection so minimizing its file size is important.

1. Remove the original remote (this will also delete all its remote tracking branches)

git remote rm origin

2. If you don't have the git remote command (because you're on a client machine with a really old git implementation) you can delete the remote branches individually using the "-r" and "-D" flags to to git branch

git branch -Dr origin/branch

3. Garbage collection. (Takes around 6 minutes)

git gc

4. You can also do aggressive garbage collection. But this takes about 15 minutes and, in testing was only able to reduce my 396MB repo to 353MB, so probably not worth it (it'd be quicker to just upload/download those extra 40MB at the client site).

git gc --aggressive

Merge in their version:
git merge -s recursive -X theirs (branch)

Abort a merge: git reset --hard HEAD

Create a new remote branch:
git checkout parentbranch
git checkout -b newbranchname
git push remotename refs/heads/newbranchname
git checkout --track -b newbranchname remotename/newbranchname

-- or the short version, to make a new branch based on your local version of a particular branch:

git push remotename yourlocalbranch:newremotebranch

Delete a remote branch
git push remotename :refs/heads/branchname

Create a new branch from the existing workspace's head (keeping any existing local changes, so you can commit them to the new branch if you want)

git checkout -b newbranchname

Then when you're done with the new branch, you can do git checkout oldbranch to go back to the old branch, and git merge newbranch to merge in the changes

Commit ranges:
start..end (In a linear history, this is "after...through". In a more complex history it's "exclude-from-here-earlier..include-from-here-earlier", or [end-and-its-parents] - [start-and-its-parents])
• means the set of commits reachable from [end] that are not reachable from [start] (by traveling along the directed parent links of [start].
• In other words, it's the set [end] and all its ancestors minus [start] and all its ancestors.
• In still other words, everything leading up to [end], minus everything leading up to [start]
• or "Give me all commits that are reachable from [end], and don't give me any commit leading up to and including [start]".

In git log you can say git log ^X Y to do the log of Y minus X and all its parents, same as git log X..Y

[start] and [end] can be: (from man git-rev-parse)
• The full SHA1 object name or a unique substring of it.
• An output from git-describe optionally follewed by a dash and a number of commits, followed by a dash, a g, and an abbreviated object name
• A symbolic ref name.
? "master" typically means commit object referenced by refs/heads/master. Also "heads/master" or "tags/master"
? "HEAD" names the commit  your changes in the working tree are based on
? FETCH_HEAD records the branch you fetched from a remote repository with your last git-fetch invocation
• a ref followed by the suffix @ with a det spec in a brace pair a879uo9087@{yesterday}, to specify the value of the ref at a prior point in time. That's your local ref at the given time.
• ref, @, and an ordinal number in braces a la {1} {5} etc, to specify nth-prior value of that ref. master@{1} is the immediate prior value of master. HEAD@{1}, likewise.
• @ with an empty ref applies to the current branch, so if you've got mdl19-stable checked out, @{1} = mdl19-stable@{1}
• @{-} means the nth branch checked out before the current one
• Suffix ^ to get the first parent of that commit (^2 for second parent, etc, would be if it's a merge with multiple parents). This can be nested so ^^ is parent of parent.
• Suffix ~ to get parent, ~2 to get parent's parent, ~3 for parent's parent, etc. ~2 = ^^. ~3 = ^^^, etc.
• :/'some text' - youngest matching commit whose message starts with specified text and is reachable from any ref.
• ref:/some/path - blob or tree at given path in the tree-ish object named by the part before the colon
• and more!
start...end (notice the three dots)
    - means give me the commits reachable from start and end, but not both. It's the XOR of their history.

You can actually list as many commits as you want. Put them without a caret prefix to include their ancestors, with a caret prefix to exclude:

^G D
^D B
^D B C
F^! D

If you mix careted and uncareted ones, it does a union of the uncareted first, then minuses all the careted ones.

Maps branch names in the remote repository to branch names in the local repository.




1. Generate a patch:
git format-patch -1

2. Apply patch:
    git am

or if that fails,

    git apply --reject

A git patch to be applied without git

    cd /var/www/source
    git diff --no-prefix HEAD~1 HEAD > patch.txt
... and then
    cd /var/www/dest
    patch --dry-run -p0

... and if that's successful
    patch -p0

Check out just a subdirectory of a git repo

Checking out just a subdirectory is a key SVN task, but git has piss-poor support for it. The recommended course is that if the subdirectory is meant to be on its own, you split it off into a separate repo, and then recombine it as a submodule. Other options are these:

1. If the repo in question is actually an SVN repo (which is why it was made with the assumption you'd check out only a subdirectory of it), you can do this by telling it that one directory is the trunk:
    git svn clone -s -r52272:HEAD -T trunk/phase3 http://svn.wikimedia.org/svnroot/mediawiki2.

If you don't mind never communicating with the parent repo ever again, you can clone the remote repo and then use git filter-branch to kill all the parts not relating to the subdirectory you're concerned with
    git filter-branch --subdirectory-filter path/to/dir -- --all
    git reset --hard
    git gc --aggressive
    git prune

Check out just some of the history of an svn rgit epo

git svn has a -Rn option, which lets you check out only certain revisions, a la
git svn clone -T path/to/trunk -R52272:HEAD

Look at the other versions during a merge
git show :1:filename - the common ancestor
git show :2:filename - the HEAD
git show :3:filename - the remote version
git checkout --ours path/to/file  shows you the version from HEAD
git checkout --theirs path/to/file  shows you the version you're merging to

The instructions it gives you when a rebase fails
(mediawiki-local-1-16-0beta2)aaronw@aaronw:~/www/moodle-itms-wiki$ git rebase rel1_16_0beta2
First, rewinding head to replay your work on top of it...
Applying: hack to allow db setup when access already configured
error: patch failed: config/index.php:224
error: config/index.php: patch does not apply
Using index info to reconstruct a base tree...
Falling back to patching base and 3-way merge...
Auto-merging config/index.php
CONFLICT (content): Merge conflict in config/index.php
Failed to merge in the changes.
Patch failed at 0001 hack to allow db setup when access already configured

When you have resolved this problem run "git rebase --continue".
If you would prefer to skip this patch, instead run "git rebase --skip".
To restore the original branch and stop rebasing run "git rebase --abort".

The instructions it gives you when a cherry-pick fails
(mdl19-hnzc)aaronw@aaronw:~/www/moodle-housingnz$ git cherry-pick d7cb528c94
Automatic cherry-pick failed.  After resolving the conflicts,
mark the corrected paths with 'git add ' or 'git rm ' and commit the result.
When commiting, use the option '-c d7cb528' to retain authorship and message.

How git merging works:
The actual details vary depending on what merge strategy you tell it to use, but the default is as follows.
1. You checkout branch A
2. You issue the command "git merge branchB"
3. Git uses "git merge-base" on the backend to find the most recent common ancestor of A & B (or if A & B have a complicated history with two most recent common ancestors that are tied, it finds the most recent common ancestor of those two). Call this ancestor C
4. Git calculates the diff of C -> B
5. Git applies this diff to A
6. If the diff cannot be applied cleanly, then you have a conflict.

How git cherrypicking works
1. You checkout branch A
2. You issue the command git cherrypick B
3. Git calculates the diff between B and B~ (which is tricky if B has multiple parents)
4. Git applies this diff to A
5. If the diff cannot be applied cleanly, you have a conflict.

The files git mergetool generates:

If you are merging branch A into branch B:

• LOCAL: The version on the local branch (A) before the merge
• BASE: The most recent common ancestor of (A) and (B). The normal git merge functionality is to calculate the diff that transforms BASE into REMOTE and apply it to LOCAL.
• REMOTE: The version on the branch (B) that's being merged in
• BACKUP: The original conflict file generated by the merge operation

If you are doing a cherrypick of commit X from branch B
• LOCAL: The version currently in HEAD
• BASE: How B looks at X~
• REMOTE: How B looks at X

If you are doing a revert of commit X from branch B
• LOCAL: The version currently in HEAD
• BASE: How B looks at X
• REMOTE: How B looks at X~

Three-way merge:


So the instructive thing is to look at the difference of Base and Remote and see how that might apply to Local

Check git logs for a REVERSE grep match:

git log --oneline 73c5bebd..HEAD | grep -vP '^[0123456789abcdef]+ Updated the 19 build version to [0-9]{8}$' | grep -oP '^[0123456789abcdef]+\b' | while read LINE; do git log --stat -n 1 ${LINE}; echo " "; done > ~/Documents/WR71594/upstream-changes-july.txt

breaking it down:
git log --oneline 73c5bebd..HEAD
? Prints the git logs in the range we're interested. The output for each one is its SHA id and subject line
grep -vP '^[0123456789abcdef]+ Updated the 19 build version to [0-9]{8}$'
? "grep -v" to remove the ones that match a particular pattern.
grep -oP '^[0123456789abcdef]+\b'
? "grep -o" to just print the SHA id for each line
while read LINE; do
? Execute a command for each line of stdin (which will have one SHA id on each line)git
git log --stat -n 1 {line}; echo " ";
? Print out each log file, with one empty line between them

Making the mysql client act like the psql client

I work a lot more with Postgres than with MySQL, consequently I'm a lot more familiar with all the ways to make the "psql" Postgress command-line client act nicely than I am with the "mysql" command-line client.

One of the things that I've often longed for in the mysql client, is psql's "\x" extended table formatting option. What "\x" does is change the display of records, so that instead of using ASCII to simulate a table in the terminal, it instead prints each field of each record on a separate line.

This comes in real handy when you're dealing with tables that have a lot of columns, which otherwise will tend to wrap in the terminal and become basically unreadable.

So for instance, without \x, the query "SELECT * FROM usr;" looks like this:

 id | username |                password                |   salt   | passwordchange | active | deleted |
expiry | expirymailsent |      lastlogin      | lastlastlogin |     lastaccess      | inactivemailsent |
staff | admin | firstname | lastname | studentid | preferredname |          email          | profileicon |
 suspendedctime | suspendedreason | suspendedcusr |  quota   | quotaused | authinstance | ctime |
showhomeinfo | logintries | unread | urlid | probation
  0 | root     | *                                      | *        |              0 |      1 |       0 |
        |              0 |                     |               |                     |                0 |
     0 |     0 | System    | User     |           |               | root@example.org        |             
|                |                 |               | 52428800 |         0 |            1 |       |
            1 |          0 |      0 |       |         0
  1 | admin    | $2a$12$Seg3PhfNnNSKEtYXY0JfyEe779zkOhW | 441d8efb |              0 |      1 |       0 |
        |              0 | 2015-11-10 14:13:06 |               | 2015-11-10 14:25:13 |                0 |
     0 |     1 | Admin     | User     |           |               | aaronw1@catalyst.net.nz |             
|                |                 |               | 52428800 |         0 |            1 |       |
            1 |          0 |      0 |       |         0
(2 rows)

 Turn on \x, and it instead looks like this:

-[ RECORD 1 ]----+---------------------------------------
id               | 0
username         | root
password         | *
salt             | *
passwordchange   | 0
active           | 1
deleted          | 0
expiry           |
expirymailsent   | 0
lastlogin        |
lastlastlogin    |
lastaccess       |
inactivemailsent | 0
staff            | 0
admin            | 0
firstname        | System
lastname         | User
studentid        |
preferredname    |
email            | root@example.org
profileicon      |
suspendedctime   |
suspendedreason  |
suspendedcusr    |
quota            | 52428800
quotaused        | 0
authinstance     | 1
ctime            |
showhomeinfo     | 1
logintries       | 0
unread           | 0
urlid            |
probation        | 0
-[ RECORD 2 ]----+---------------------------------------
id               | 1
username         | admin
password         | $2a$12$Seg3PhfNnNSKEtYXY0JfyEe779zkOhW
salt             | 441d8efb
passwordchange   | 0
active           | 1
deleted          | 0
expiry           |
expirymailsent   | 0
lastlogin        | 2015-11-10 14:13:06
lastlastlogin    |
lastaccess       | 2015-11-10 14:25:13
inactivemailsent | 0
staff            | 0
admin            | 1
firstname        | Admin
lastname         | User
studentid        |
preferredname    |
email            | aaronw1@catalyst.net.nz
profileicon      |
suspendedctime   |
suspendedreason  |
suspendedcusr    |
quota            | 52428800
quotaused        | 0
authinstance     | 1
ctime            |
showhomeinfo     | 1

It uses a lot more vertical space, but you can actually read it with your naked eyes and tell which value matches up with which column.

Well, having to debug Mahara's current problem with MySQL deadlocks, today I decided to figure it out. Taking a look through the mysql client's internal help and man pages, I discovered there's a "\G" command that will cause it to render the output in vertical format, that looks a lot like psql. \G is a bit strange. It's not a setting toggle like psql's \x. Instead, you use it in place of a semicolon to end your query.

mysql> select * from usr limit 1 \G
*************************** 1. row ***************************
              id: 0
        username: root
        password: *
            salt: *
  passwordchange: 0
          active: 1
         deleted: 0
          expiry: NULL
  expirymailsent: 0
       lastlogin: NULL
   lastlastlogin: NULL
      lastaccess: NULL
inactivemailsent: 0
           staff: 0
           admin: 0
       firstname: System
        lastname: User
       studentid: NULL
   preferredname: NULL
           email: root@example.org
     profileicon: NULL
  suspendedctime: NULL
 suspendedreason: NULL
   suspendedcusr: NULL
           quota: 52428800
       quotaused: 0
    authinstance: 1
           ctime: NULL
    showhomeinfo: 1
      logintries: 0
          unread: 0
           urlid: NULL
       probation: 0
1 row in set (0.00 sec)

I quickly discovered, however, that this doesn't automatically go to a pager like psql's "\x". So you you return more rows than will fit on the screen, they'll all zip off to outer space. However, MySQL does have a "\P" option to set your pager command. So if you do "\P less", then you'll send your output to "less", and be able to page up and down and search and all that good stuff. In fact, the mysql man page suggests you use "\P less -n -i -S -F -X", which will add line numbers, case-insensitive search, no wrapping of long lines, *and* print the results directly to the screen if they're short enough to fit all on one screen.

I find this easier to remember as "\P less -SinFX". ;)

In fact, with the "-S" option to disable line wrapping, you don't even really need the vertical output format anymore! But, if you do really like it, you can turn it on by default in mysql by using the flag "mysql --auto-vertical-output", which is even smarter than just toggling "\x" in psql, because it will automatically switch you to the vertical output mode when your query is too wide to fit on the screen.

This got me thinking, well, if mysql can do that, can't psql? So I dug through the psql man page and noticed this command:

\pset expanded auto

... which, as of psql version 9.2, will do the same thing, making psql switch between vertical mode and tabular mode depending on how wide your query output results are.

So in the end, I've discovered that one of the things I preferred most about the psql client can be done in mysql as well. And I've improved my ability to use the psql client at the same time. These are things that can happen when you read the man page, even for utilities you use every day!

And of course if you find some mysql/psql client options that you use every time, you can pop them in your "~/.my.cnf" or "~/.psqlrc" file, and/or use a shell alias, to make them happen every time.

Mahara has a VERP system!

I've been working on Mahara for about a year now, and I'm still frequently surprised by the discover of new features in it. One recent one, was that Mahara has a fully functional VERP system for handling bounced emails.

About a month ago I was discussing on the forums the possibility of adding reply-by-email functionality to the Mahara forums and Mahara inter-person messages. It's a fairly frequently requested feature, but, I said, the problem is that you'd need to implement a lot of infrastructure to handle it, specifically, you've gotta do VERP, a system by which you provide a different Reply-To address to each message, with identifying information encoded in the address. Then when a person replies by email, you can figure out what they were replying to and authenticate them to some extent, just by looking at the address they sent to. You also need to set up infrastructure for receiving these response emails either by checking an IMAP account or by setting up your incoming SMTP server to pipe messages to your own command-line script.

That'd be great to have, but it's a lot of work so it was unlikely to materialize any time soon.

That's what I thought, anyway. Then, while looking through the list of cron tasks in Mahara's "cron" table, I noticed one that called the function "check_imap_for_bounces()". On investigation it became apparent that Mahara already has a fully functional VERP system, fully documented in the config-defaults.php file, and accessible by SMTP script or IMAP! Not only that, but the IMAP version has been there since Mahara 1.6, and the SMTP version since 1.3!

Admittedly, the documentation for this is still a little sparse. As mentioned, all the necessary configuration elements are described in the config-defaults.php file, which is a good start. If you're not familiar with config-defaults.php, you should check it out. It's actually htdocs/lib/config-defaults.php. This is a file which defines default values for many Mahara configuration parameters, as well as describing many others that are left undefined by default.

Other than that, the only documentation for it is a page on the Mahara wiki, which still inaccurately describes it as a feature "under development", even though it's been complete and working for years now: https://wiki.mahara.org/index.php/Developer_Area/Specifications_in_Development/VERP_Mail_Bounces

I immediately wondered why we weren't already using this on mahara.org, a site that receives hundreds or thousands of bounce emails each day. And apparently it had been set up at some point in the past, but had apparently been forgotten about and disabled since then, probably during a big migration in the site's server and deployment system around December 2012. I re-enabled it (using IMAP polling, because that seems more likely to continue operating even if we forget about it or move servers again), and it's been working like a charm.

I don't believe this is an uncommon experience, when working on a project as long-lived and large as Mahara (and Mahara's scope is actually relatively small when compared to other projects, like Moodle). Mahara has hundreds of thousands of lines of PHP code, and hundreds of features, more than anyone can easily encompass. What can you do about that? One thing is to code defensively and avoid breaking internal API's. Automated testing would be nice, and we're working on that. And listening to the community -- often that's one of the main ways I discover new features... and find out that they've been broken.

Figuring out the buttons on the Kensington Expert Trackball in Ubuntu

The short version:

In GNOME, the default meanings of the Kensington Expert Trackball's four buttons are:

  • Bottom-left: Left-click
  • Bottom-right: Right-click
  • Upper-right: Middle-click
  • Upper-left: Browser back

If, like me, you've used the Mouse control panel to switch things to left-handed mode, these are flipped left-right.


The long version:

I'm a trackball user. My workmates think I just do it to make it harder for them to use my computer. But the truth is, whenever I have to use a mouse for any length of time, my hand cramps up from the inevitable repetitive task of lifting the mouse up with my thumb and the side of my pinky to reposition it in the middle of the mousing area. I've been using a Logitech trackball happily for the past couple of years. Logitech only makes two models of trackball -- mine's the one that only fits your right hand. After an inspection by a workplace ergonomics consultant, I was advised to switch to mousing with my left hand to alleviate some issues in my right shoulder. So, I've switched to the venerable "Kensington Expert Trackball", an ambidextrous trackball with four buttons.

The tricky part is, what do those buttons do? Kensington offers configuration software, but for Windows only. A little bit of experimentation found that the bottom two buttons were the "right-click" and "left-click" buttons, but the other two were a little more enigmatic.

The first really helpful page I found on this subject, was one about mapping mouse buttons in Fedora. It advised me to use a program called xev to find out what Gnome button each button was mapped to. Clicking the upper-left and upper-right buttons in xev gave me (among other things) this output:

ButtonPress event, serial 33, synthetic NO, window 0x6400001,
    root 0x25d, subw 0x6400002, time 934193039, (51,37), root:(1978,83),
    state 0x0, button 2, same_screen YES

ButtonPress event, serial 33, synthetic NO, window 0x6400001,
    root 0x25d, subw 0x6400002, time 934193751, (51,37), root:(1978,83),
    state 0x0, button 8, same_screen YES

(I also later found out from here that I could have achieved the same thing by using xinput.) So now I knew the buttons were interpreted as "button 2" and "button 8". But what do those do?

From another post about the Logitech Marble Mouse (Logitech's ambidextrous mouse, which I was considering as an alternative to the Kensington), I found that Button 2 is equivalent to the middle button on a 3-button mouse, and Button 8 is "browser back".

Which left me with just one more unanswered question: What the heck does the middle button a 3-button mouse do, in Ubuntu? I found the answer here. The short version: A lot of stuff! Probably the most useful and easy to remember, are that it opens links in a new browser tab, and it copy-and-pastes highlighted text.

Probably the next thing most technical users would be interested in is, how do I change what these buttons do? Well, there's plenty of information about that on the web. A couple of good places to start:

In my case, the only modification I needed to make was to swap the buttons for left-handed use. And fortunately in Ubuntu, you can do that with the "Mouse" control panel!