Skip to content

fix branch detection on empty repo#696

Merged
vilmibm merged 14 commits intomasterfrom
pr-status-no-commits
Mar 30, 2020
Merged

fix branch detection on empty repo#696
vilmibm merged 14 commits intomasterfrom
pr-status-no-commits

Conversation

@vilmibm
Copy link
Copy Markdown
Contributor

@vilmibm vilmibm commented Mar 24, 2020

closes #682

This adds a kind of gross hack to enable branch detection on a repo with no commits. You are "on" a
branch; but without commits rev-parse fails. branch is also not helpful. i noticed that log
tells you the branch you are on (that is lacking commits), so I extracted the branch name from the
error.

This adds a fallback to symbolic-ref when doing branch detection so that it works on an empty repository. symbolic-ref is defined as far back as git 1.9.

testing changes

I took this opportunity to touch utils.Runnable and CmdStubber some more:

  • moved the prepare cmd stuff (including the Runnable interface) into its own package,
    internal/run
  • added StubError to CmdStubber. This is primitive in that it's working with a raw error
    instead of CmdErr but I figure it was enough for now.

@vilmibm vilmibm requested a review from mislav March 24, 2020 19:30
Copy link
Copy Markdown
Contributor

@mislav mislav left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Love the cmd stubber extraction into a new internal package, but I think we need to tweak our approach still to detecting the branch name in a no-commits scenario.

git/git.go Outdated

// CurrentBranch reads the checked-out branch for the git repository
func CurrentBranch() (string, error) {
err := run.PrepareCmd(GitCommand("log")).Run()
Copy link
Copy Markdown
Contributor

@mislav mislav Mar 24, 2020

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Consider that git log command will, with no extra arguments, list all the commits reachable from current HEAD. In large repositories, this is an expensive operation.

We could potentially limit the output by adding -1, but we could also explore options to use a lower-level approach than git log.

git/git.go Outdated
err := run.PrepareCmd(GitCommand("log")).Run()
if err != nil {
// this is a hack.
errRe := regexp.MustCompile("your current branch '([^']+)' does not have any commits yet")
Copy link
Copy Markdown
Contributor

@mislav mislav Mar 24, 2020

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I would suggest that we avoid ever matching text in error messages. I have two main reasons:

  1. Forward-compatibility: future git versions might decide to tweak an error message;
  2. Localization-sensitivity: not everybody has git in English.
$ LANG=es git rev-list -1 HEAD
fatal: argumento ambiguo 'HEAD': revisi'on desconocida o ruta fuera del 'arbol de trabajo.
Use '--' para separar las rutas de las revisiones, de esta manera:
'git <comando> [<revisi'on>...] -- [<archivo>...]'

If we wanted to detect whether the current branch has any commits, I think we could do git rev-list -1 HEAD, silence stderr, and see if there is a line on stdout.


So this leaves a problem on detecting the branch name when we're on a branch that has no commits yet. We have several options:

  1. git branch --show-current: works, but not present in the oldest git version we support (1.9);
  2. Scanning the branch name from git log error message: brittle;
  3. Parsing the branch name from .git/HEAD file manually: possible, but not straightforward to pull off since .git can be a regular file in case of git worktrees;
  4. Avoiding reading the current branch name and just failing with git: not on any branch, basically equating the no-commits scenario with detached HEAD scenario;
  5. git symbolic-ref --short HEAD : might do the job, but we'd have to check how far back it is supported, otherwise this is in the same basket as (1).

I'm leaning to (3) as the most "correct" approach, (5) as something to look into, and (4) as a lazy approach.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

All very fair criticisms; I knew it was a bad solution but figured you'd have a good suggestion on a different approach ^_^() I just don't have enough lower level git knowledge yet.

git symbolic-ref exists as far back as git 1.9 and I like using it more than the other options. I was going to suggest we switch to that by default instead of the existing rev-parse invocation but it errors on detached HEAD :(

I'll clean up CurrentBranch to use both, I guess.

Calls []*exec.Cmd
}

func UseTempGitRepo() *TempGitRepo {
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔥🔥🔥

@vilmibm vilmibm requested a review from mislav March 25, 2020 17:02
Copy link
Copy Markdown
Contributor

@mislav mislav left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is looking great! One last thought about simplifying git invocations

git/git.go Outdated
// we avoid using `git branch --show-current` for compatibility with git < 2.22
branchCmd := exec.Command("git", "rev-parse", "--abbrev-ref", "HEAD")
output, err := run.PrepareCmd(branchCmd).Output()
output, err := run.PrepareCmd(GitCommand("rev-parse", "--abbrev-ref", "HEAD")).Output()
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This approach looks like it might cover all the cases, but I wonder if we could get away with using just a single invocation without fallbacks? It looks like git symbolic-ref --quiet --short HEAD could be just what we need:

  • exit code == 0 + lines on stdout: the branch name was determined successfully
  • exit code != 0 + no stderr: detached HEAD (no current branch)
  • exit code != 0 + lines on stderr: some other error with which we should abort the process

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

great thought, implemented.

@vilmibm vilmibm requested a review from mislav March 26, 2020 20:36
Copy link
Copy Markdown
Contributor

@mislav mislav left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

…one last tiny bit! 😅

git/git.go Outdated
output, err = run.PrepareCmd(GitCommand("symbolic-ref", "--short", "HEAD")).Output()
if err != nil {
return "", noBranchError
ce := err.(*run.CmdError)
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is going to panic if err is not of type *run.CmdError.

As a rule of thumb, we should never assume that an error is guaranteed to be of any specific type, but always check the type:

var cmdErr *run.CmdError
if errors.As(err, &cmdErr) {
	cmdErr.Stderr.Len()
}

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

hm; I opted for the panic because it seemed like err was always supposed to be a *run.CmdError and panicking would signal to a developer that they were changing that contract. If we silently ignore the failed cast, doesn't that mean we can let a bug go unacknowledged?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ah, re-reading more closely i see it's possible to not get a CmdError out of a prepared command. I'll fix the type assertion.

test/helpers.go Outdated
type OutputStub struct {
Out []byte
Out []byte
Error *run.CmdError
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should this be of type error so we can return any error type that we want, instead of just *run.CmdError?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

like i said above I misread the code as always using CmdError and didn't see the need for that flexibility; i'll update it.

@vilmibm vilmibm requested a review from mislav March 27, 2020 16:35
Copy link
Copy Markdown
Contributor

@mislav mislav left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Who would have thought that reading the current branch name could be so tricky 😣 I hope that in the future we can revise our minimum git support and, after all popular OSs ship with a git version newer than 1.9, we can migrate to git branch --show-current.

Thanks for being patient and working with me through all the iterations! 🌟

@vilmibm vilmibm merged commit 7555a4c into master Mar 30, 2020
@mislav mislav deleted the pr-status-no-commits branch June 25, 2020 14:37
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Support pr status in a repository that doesn't have any commits

2 participants