The short answer is "no", but the question itself is a bit problematic. If you ask the right question the answer might become "yes".
First, "pull request" is not a Git thing—it's an add-on offered by various web services, such as GitHub or Bitbucket or (in your case) Azure. What Git really has is the ability to fetch commits—obtain commits intact from some other Git repository—and to merge.
When you fetch someone else's commits, what you get is quite literally their commits. Every commit in the universe has its own distinct, unique hash ID. The hash ID is a cryptographic checksum of everything that went into the commit: all the files in the snapshot, the name and email address of the person who made the commit, their log message, and—crucially for Git—all of the history that led up to this point in time. That is, in order to put this commit into your repository, you must also take all of the commits—with their snapshots and their authors and log messages and so on—that led up to this commit.
Now that you have their commits in your repository, you have their commits. It is now up to you to decide what you wish to do with those commits. You can keep them as is, or you can make copies of them and make changes while you're in the middle of copying (before you commit the copies). These copies can have any differences you like: just remember that a copy will have a different hash ID than the original. Only the original commits can use the original hash IDs.
If you keep the originals, you keep their file structure. There is no way around this. A commit, with its unique hash ID, is frozen for all time. No one—not you, not them, not Git—can change that commit. You either have it, and it's that way, or you don't have it at all. (You can achieve the "don't have it at all" state by deciding, after you've stuffed these commits into your repository, that you don't like them. You just stop using them and referring to them by their hash IDs, and eventually your Git discards them. There's some trickiness here with references and reflogs, but mostly it's just a matter of taking away any references and waiting.)
If you copy those originals to new commits with new file structures, that's fine. You can keep your copies whether or not you keep the originals. However, your copies are just that—yours—and they won't combine well with future updates from them, whoever they are. If you intend to do ongoing work with these other people, that's probably not a good path.
Let's look now at the second, more interesting part:
Is there a way to manually override the pull request so that mapping files from (A) to (B) can occur where (A)'s files map to (B)'s files? Again they're the same files just with a different parent folder. I'd like to avoid changing (A)'s folder structure if I can help it.
Now that we know that there's no such thing as a pull request in Git, we can turn this into the right question, which is:
Now that I have their commits in my repository, can I merge their commits with my commits using parameters that loosen Git's matching rule for files?
The answer to this question is yes. You probably need to do this with command-line Git, rather than with a fancy web interface—GitHub's clicky button web interface does not have the ability, for instance.
When Git is performing a merge (as in git merge otherbranch
), there are three inputs to this merge. One of the three inputs is your current commit—the tip of the branch you're on, or the HEAD
commit: these are two names for the same commit, whose true name is its big ugly hash ID. One of the inputs is the other commit you've specified—otherbranch
in this case, but you can also just give a raw hash ID; Git just turns the name otherbranch
into a raw hash ID, for the purpose of the merge.
That's two inputs, so what is the third? The answer is that it is implied by the graph. Remember above where I said that if you take one specific commit from someone else, you must also take all the commits leading up to that one specific commit. We can draw this situation graphically:
...--o--o--*--o--o--L <-- yourbranch (HEAD)
\
A--B--R <-- theirbranch (or theirrepo/branch or whatever)
Here L
stands for your current (Left or Local or --ours
) commit, and R
stands for their (Right or otheR or obtained-from-Remote-Git or --theirs
) commit. A
and B
are stand in for the hash IDs of the commits you were required to get from them in order to get commit R
, and *
is the hash ID of the parent of their commit A
that you already had.1
The way git merge
works, for these true merge cases—yours will definitely be one such—is that Git runs two git diff
s, to figure out what you changed and what they changed. The first diff is, in effect:
git diff --find-renames <hash-of-*> <hash-of-L>
Note this --find-renames
argument. The second diff is the equivalent of:
git diff --find-renames <hash-of-*> <hash-of-R>
If you did not rename a folder, between *
and L
, and they did rename a folder, between *
and L
, Git will attempt—during the merge—to match up files in *
and R
even though they have different names. This attempt depends on the similarity of the files' contents.
Meanwhile, if you renamed a folder, between *
and L
, and they did not rename that folder, Git does the exact same thing. It tries to match up the base names in *
with your names in L
. This attempt depends on the similarity of the files' contents.
If you both renamed folders, that's OK too. Git tries to find the original file, in commit *
, based on its content similarity to the contents of each of the new names of the maybe-same-maybe-not files in the two branch tips.
Having paired-up all the renamed files in *
and L
and found that file path/to/file.ext
in *
is now path/different/file.ext
in L
, Git knows that the changes you made to file.ext
are those obtained by comparing *
's original file.ext
to L
's new name for the same file. It also knows that you renamed the file. Similarly, having paired-up all the renames from *
to R
, Git knows that the changes they made to file.ext
are those obtained by comparing *
's original file.ext
to R
's new name for the same file.
In all cases, once Git has identified the renamed files correctly, the merge proceeds as usual: Git tries to combine your changes and their changes, file-by-file. It also tries to keep any renames that either of you did.
This can all go wrong in several ways:
If you both renamed file.ext
, Git doesn't know which new name to keep. You will get a rename/rename
conflict which you will have to solve manually. This is separate from any other merge conflicts that you also have to resolve on your own. When you are done resolving, git mv
the file if necessary, to give it the name you want to keep after all, and git add
the combined changes under the name you want to keep.
If whoever changed the file's name also changed the contents too much, Git won't be able to pair up the old and new files. How much is too much? Well, Git has the concept of a similarity threshold. When Git is doing the --find-renames
part of a git diff old-commit new-commit
operation, Git will, for each file that seems to have been deleted from the old commit, compare the deleted file's contents to the contents of every file that seems to have been created from scratch in the new commit. If the old file.ext
is 30% similar to a new different.ext
and 70% similar to a new other.ext
, the 70%-similar match wins. But if no file reaches a "50% match", the default is to decide that the file was deleted after all.
If you run git diff --find-renames
yourself, you can add a rename threshold factor, which defaults to 50% but is adjustable. Adjust it up or down as needed to make Git pair up the right files. Git will tell you, in its diff output, what the similarity index was.
You can run this sort of git diff
manually before you run git merge
, and find the proper similarity index that makes Git match up the files. You can then run git merge -X find-renames=number
to tell git merge
to use that number for both of its git diff --find-rename
operations.
Of course, if you have to lower the similarity threshold a lot, there is a good chance that the merge operation itself may have conflicts here, because that suggests that you've changed the file so much that any changes they make will clash will probably clash with the changes you made. But that may be sufficient to handle more of the merge automatically.
So, the recipe, as it were, here is to do the merge manually. First use git fetch
to obtain the commits that you are proposing to merge. Then, use git merge-base --all
to find the shared merge-base commit that git merge
will find. Run git diff --find-renames
using this shared merge-base as the starting point commit, and your and/or their branch tip commit hash ID or branch name as the ending point commit. Add --name-status
to this git diff
to get just a summary of which files have been paired up and been found modified, vs which are considered deleted. Tweak the rename-finding threshold (--find-renames=number
, or -Mnumber
if you want to use the short spelling) until you get the best possible results. Then use git merge
with the -X rename-threshold=number
option to make git merge
pass that same number through to the two underlying diffs.
1It's possible you already had A
and B
anyway. What makes commit *
important is that it's the best shared commit: of all of the commits that are on both your branch and their branch, it's the best of those. Technically it is the Lowest Common Ancestor (LCA) commit of the two selected commits within the Directed Acyclic Graph (DAG) of commits that make up your repository. You can find the hash ID of this commit using:
git merge-base --all HEAD otherbranch
for instance. Sometimes there is no common commit at all, and sometimes—rarely—there is more than one LCA of two commits in the DAG, but typically this produces just one hash ID, and that's the merge base.