9

I am trying to implement something that will give me the outcome of git pull or git fetch && git merge. I got this working in part, but the problem I am having is that after running the following code, the current repo is left thinking that there are local changes to be committed.

As far as I can see, I believe I am probably not creating the annotated commit from the correct HEAD or I need to make another commit? (I am not sure).

My code looks like this and I am stuck:

func (repo *Repo) Pull() error {
    // Get remote
    remote, err := repo.Remotes.Lookup("origin")
    if err != nil {
        remote, err = repo.Remotes.Create("origin", repo.Path())
        if err != nil {
            return err
        }
    }

    // Get the branch
    branch, err := repo.Branch()
    if err != nil {
        return err
    }

    // Get the name
    branchName, err := branch.Name()
    if err != nil {
        return err
    }

    if err := remote.Fetch([]string{}, &git.FetchOptions{}, ""); err != nil {
        return err
    }

    // Merge
    remoteRef, err := repo.References.Lookup("refs/remotes/origin/" + branchName)
    if err != nil {
        return err
    }

    mergeRemoteHead, err := repo.AnnotatedCommitFromRef(remoteRef)
    if err != nil {
        return err
    }

    mergeHeads := make([]*git.AnnotatedCommit, 1)
    mergeHeads[0] = mergeRemoteHead
    if err = repo.Merge(mergeHeads, nil, nil); err != nil {
        return err
    }

    return nil
}

After running it, I get the changes from remote merged and the working directory updated, but it tells me I need to make a commit.

I think I have a similar issue as the OP of this question.

Community
  • 1
  • 1
Sthe
  • 2,575
  • 2
  • 31
  • 48

2 Answers2

7

That's the way libgit2's git_merge function is documented:

Any changes are staged for commit and any conflicts are written to the index. Callers should inspect the repository's index after this completes, resolve any conflicts and prepare a commit.

So you have to add code to check whether the index has conflicts. If there are none, you may commit what is staged. Otherwise you probably would want to prompt the user to resolve the conflicts.

sschuberth
  • 28,386
  • 6
  • 101
  • 146
  • Could you elaborate a bit on why I need to create a commit? These are incoming changes from `origin`. Should they be getting staged? Should it not just replay those changes and leave me with a clean working directory (given that no changes we made locally). This function stages changes not made locally, and does not record commits pulled down from remote. – Sthe Oct 18 '15 at 15:18
  • libgit2 (and its bindings) operate on a lower level than the git (porcelain) command line. In particular, git2go's `Merge` does much less that `git merge`. While `git merge` by default is able to simplify to fast-forward merges (which in fact are no merges at all, see `--ff`), git2go's `Merge` explicitly creates a merge, even if a fast-forward was possible. A merge in turn always creates a merge commit, and in order to commit something, the index has to be populated. Again git2go's `Merge` is far more low-level and requires you to explicitly commit even if there are no conflicts. – sschuberth Oct 19 '15 at 07:39
5

Using input from sschuberth's answer above, I was able to put together a working version of this function and I thought I'd share the breakthrough. As pointed out, the repo.Merge() function in libgit2 (as Git2go in this case) doesn't do nearly as much as git pull does. Let me explain it one step at a time (I stand corrected):

As explained here, git pull actually does a git fetch and then a git merge and that is what we gonna do:

Locate the remote to retrieve changes from

remote, err := repo.Remotes.Lookup("origin")
if err != nil {
    return err
}

Fetch changes from remote

if err := remote.Fetch([]string{}, nil, ""); err != nil {
    return err
}

Get the corresponding remote reference

remoteBranch, err := repo.References.Lookup("refs/remotes/origin/branch_name")
if err != nil {
    return err
}

You now have the changes from the remote but you need to let Git tell you how to deal with them further. So, do a merge analysis.

Perform a merge analysis

annotatedCommit, err := repo.AnnotatedCommitFromRef(remoteBranch)
if err != nil {
    return err
}

// Do the merge analysis
mergeHeads := make([]*git.AnnotatedCommit, 1)
mergeHeads[0] = annotatedCommit
analysis, _, err := repo.MergeAnalysis(mergeHeads)
if err != nil {
    return err
}

Now, you need to check the value of analysis to see which status value it points to and do the merge accordingly.

Test the returned value

Be sure to do the test on the binary level, so use the bitwise operators. For example:

if analysis & git.MergeAnalysisUpToDate != 0 {
    return nil
}

There is nothing to do here (in my case). Everything is up to date.

else if analysis & git.MergeAnalysisNormal != 0 {
    // Just merge changes
    if err := repo.Merge([]*git.AnnotatedCommit{annotatedCommit}, nil, nil); err != nil {
        return err
    }
    // Check for conflicts
    index, err := repo.Index()
    if err != nil {
        return err
    }

    if index.HasConflicts() {
        return errors.New("Conflicts encountered. Please resolve them.")
    }

    // Make the merge commit
    sig, err := repo.DefaultSignature()
    if err != nil {
        return err
    }

    // Get Write Tree
    treeId, err := index.WriteTree()
    if err != nil {
        return err
    }

    tree, err := repo.LookupTree(treeId)
    if err != nil {
        return err
    }

    localCommit, err := repo.LookupCommit(head.Target())
    if err != nil {
        return err
    }

    remoteCommit, err := repo.LookupCommit(remoteBranchID)
    if err != nil {
        return err
    }

    repo.CreateCommit("HEAD", sig, sig, "", tree, localCommit, remoteCommit)

    // Clean up
    repo.StateCleanup()
}

In short, the code block above just performs a merge and tests for conflicts after. If any conflicts are encountered, deal with them (prompt the user perhaps). This will result in uncommitted changes, so be sure to create a commit after.

else if analysis & git.MergeAnalysisFastForward != 0 {
    // Fast-forward changes
    // Get remote tree
    remoteTree, err := repo.LookupTree(remoteBranchID)
    if err != nil {
        return err
    }

    // Checkout
    if err := repo.CheckoutTree(remoteTree, nil); err != nil {
        return err
    }

    branchRef, err := repo.References.Lookup("refs/heads/branch_name")
    if err != nil {
        return err
    }

    // Point branch to the object
    branchRef.SetTarget(remoteBranchID, "")
    if _, err := head.SetTarget(remoteBranchID, ""); err != nil {
        return err
    }

}

On the code above, there isn't anything to merge. You just need to replay the changes from remote onto your local and updated where HEAD is pointing.

The above was sufficient for me. I hope the approach helps you too. Find the complete function on this gist

Sthe
  • 2,575
  • 2
  • 31
  • 48