Skip to content

Commit

Permalink
New hook to prevent bookmark creation by prefix
Browse files Browse the repository at this point in the history
Summary:
Git has this limitation that since refs are stored in the file system, a user cannot create two refs like `refs/heads/some_branch` and `refs/heads/some_branch/another` because storing the latter ref would require creating folders `refs`, `heads` and `some_branch` while creating the former would require creating `some_branch` as a file. We cannot have a file and a directory with the same name at the same level, hence this behavior is disallowed.

Mononoke does not prevent this by default since refs (bookmarks) in Mononoke are stored as entries in a DB table so there is no such restriction. However, to maintain parity with vanilla Git, we have to put this check in place as a hook.

Reviewed By: andreacampi

Differential Revision: D66768700

fbshipit-source-id: 8d80ff324248f8bc06201a3e57e3194aa46afdbf
  • Loading branch information
RajivTS authored and facebook-github-bot committed Dec 9, 2024
1 parent 5c1ef0b commit 94f4603
Show file tree
Hide file tree
Showing 4 changed files with 252 additions and 0 deletions.
6 changes: 6 additions & 0 deletions eden/mononoke/hooks/src/implementations.rs
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ mod block_files;
pub(crate) mod block_invalid_symlinks;
pub(crate) mod block_merge_commits;
pub(crate) mod block_new_bookmark_creations_by_name;
pub(crate) mod block_new_bookmark_creations_by_prefix;
pub(crate) mod block_unannotated_tags;
pub(crate) mod block_unclean_merge_commits;
pub(crate) mod deny_files;
Expand Down Expand Up @@ -66,6 +67,11 @@ pub async fn make_bookmark_hook(
&params.config,
)?,
)),
"block_new_bookmark_creations_by_prefix" => Some(b(
block_new_bookmark_creations_by_prefix::BlockNewBookmarkCreationsByPrefixHook::new(
&params.config,
)?,
)),
"block_unannotated_tags" => {
Some(b(block_unannotated_tags::BlockUnannotatedTagsHook::new()))
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,120 @@
/*
* Copyright (c) Meta Platforms, Inc. and affiliates.
*
* This software may be used and distributed according to the terms of the
* GNU General Public License version 2.
*/

use std::str::FromStr;

use anyhow::Error;
use anyhow::Result;
use async_trait::async_trait;
use bookmarks::BookmarkKey;
use bookmarks::BookmarkPrefix;
use context::CoreContext;
use mononoke_types::BonsaiChangeset;
use mononoke_types::MPath;
use serde::Deserialize;

use crate::BookmarkHook;
use crate::CrossRepoPushSource;
use crate::HookConfig;
use crate::HookExecution;
use crate::HookRejectionInfo;
use crate::HookStateProvider;
use crate::PushAuthoredBy;

#[derive(Clone, Debug, Deserialize)]
pub struct BlockNewBookmarkCreationsByPrefixConfig {
message: Option<String>,
}

#[derive(Clone, Debug)]
pub struct BlockNewBookmarkCreationsByPrefixHook {
config: BlockNewBookmarkCreationsByPrefixConfig,
}

impl BlockNewBookmarkCreationsByPrefixHook {
pub fn new(config: &HookConfig) -> Result<Self> {
Self::with_config(config.parse_options()?)
}

pub fn with_config(config: BlockNewBookmarkCreationsByPrefixConfig) -> Result<Self> {
Ok(Self { config })
}
}

#[async_trait]
impl BookmarkHook for BlockNewBookmarkCreationsByPrefixHook {
async fn run<'this: 'cs, 'ctx: 'this, 'cs, 'fetcher: 'cs>(
&'this self,
ctx: &'ctx CoreContext,
bookmark: &BookmarkKey,
_from: &'cs BonsaiChangeset,
content_manager: &'fetcher dyn HookStateProvider,
_cross_repo_push_source: CrossRepoPushSource,
_push_authored_by: PushAuthoredBy,
) -> Result<HookExecution, Error> {
let bookmark_state = content_manager.get_bookmark_state(ctx, bookmark).await?;
if !bookmark_state.is_new() {
return Ok(HookExecution::Accepted);
}
// Ensure we append a trailing slash if the bookmark doesn't have one. This is because
// we are trying to check if the bookmark matches any existing bookmarks as a path component
// e.g. some/bookmark/ matching some/bookmark/path.
let bookmark_prefix_str = if !bookmark.as_str().ends_with("/") {
format!("{bookmark}/")
} else {
bookmark.to_string()
};
// Check if this bookmark itself is a path prefix of any existing bookmark
let bookmark_prefix = BookmarkPrefix::from_str(bookmark_prefix_str.as_str())?;
if content_manager
.bookmark_exists_with_prefix(ctx.clone(), &bookmark_prefix)
.await?
{
if let Some(message) = &self.config.message {
return Ok(HookExecution::Rejected(HookRejectionInfo::new_long(
"Invalid bookmark creation is restricted in this repository.",
message.clone(),
)));
} else {
return Ok(HookExecution::Rejected(HookRejectionInfo::new_long(
"Invalid bookmark creation is restricted in this repository.",
format!(
"Creation of bookmark \"{bookmark}\" was blocked because it exists as a path prefix of an existing bookmark",
),
)));
}
}
// The current bookmark is not a path prefix of any existing bookmark, so check if any of its path
// prefixes exist as a bookmark for this repo.
for bookmark_prefix_path in MPath::new(bookmark_prefix_str.as_str())?.into_ancestors() {
let bookmark_prefix_path =
BookmarkKey::from_str(std::str::from_utf8(&bookmark_prefix_path.to_vec())?)?;
// Check if the path ancestors of this bookmark already exist as bookmark in the repo
if content_manager
.get_bookmark_state(ctx, &bookmark_prefix_path)
.await?
.is_existing()
{
if let Some(message) = &self.config.message {
return Ok(HookExecution::Rejected(HookRejectionInfo::new_long(
"Invalid bookmark creation is restricted in this repository.",
message.clone(),
)));
} else {
return Ok(HookExecution::Rejected(HookRejectionInfo::new_long(
"Invalid bookmark creation is restricted in this repository.",
format!(
"Creation of bookmark \"{bookmark}\" was blocked because its path prefix \"{bookmark_prefix_path}\" already exists as a bookmark",
),
)));
}
}
}

Ok(HookExecution::Accepted)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,10 @@ impl BookmarkState {
}
false
}

pub fn is_existing(&self) -> bool {
!self.is_new()
}
}

/// Enum describing the type of a tag for which hooks are being run.
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,122 @@
# Copyright (c) Meta Platforms, Inc. and affiliates.
#
# This software may be used and distributed according to the terms of the
# GNU General Public License found in the LICENSE file in the root
# directory of this source tree.

$ . "${TEST_FIXTURES}/library.sh"

$ REPOTYPE="blob_files"
$ setup_common_config $REPOTYPE
$ GIT_REPO_ORIGIN="${TESTTMP}/origin/repo-git"
$ GIT_REPO_SUBMODULE="${TESTTMP}/origin/repo-submodule-git"
$ GIT_REPO="${TESTTMP}/repo-git"

# Setup git repository
$ mkdir -p "$GIT_REPO_ORIGIN"
$ cd "$GIT_REPO_ORIGIN"
$ git init -q
$ echo "this is file1" > file1
$ git add file1
$ git commit -qam "Add file1"
$ old_head=$(git rev-parse HEAD)
$ git tag -a -m"new tag" first_tag
$ echo "this is file2" > file2
$ git add file2
$ git commit -qam "Add file2"
$ git tag -a empty_tag -m ""
$ cd "$TESTTMP"
$ git clone "$GIT_REPO_ORIGIN"
Cloning into 'repo-git'...
done.

# Import it into Mononoke
$ cd "$TESTTMP"
$ quiet gitimport "$GIT_REPO" --derive-hg --generate-bookmarks full-repo

# Set Mononoke as the Source of Truth
$ set_mononoke_as_source_of_truth_for_git

$ cd "$TESTTMP"/mononoke-config
$ cat >> repos/repo/server.toml <<EOF
> [[bookmarks]]
> regex=".*"
> [[bookmarks.hooks]]
> hook_name="block_new_bookmark_creations_by_prefix"
> [[hooks]]
> name="block_new_bookmark_creations_by_prefix"
> config_json='''{
> }'''
> bypass_pushvar="x-git-allow-invalid-bookmarks=1"
> EOF
$ cd "${TESTTMP}"

# Start up the Mononoke Git Service
$ mononoke_git_service
# Clone the Git repo from Mononoke
$ git_client clone $MONONOKE_GIT_SERVICE_BASE_URL/$REPONAME.git
Cloning into 'repo'...

# Add some new commits to the cloned repo and push it to remote
$ cd repo
$ echo new_file > new_file
$ git add .
$ git commit -qam "Commit"

# This push works
$ git_client push origin --all
To https://localhost:$LOCAL_PORT/repos/git/ro/repo.git
e8615d6..8ff9b0a master_bookmark -> master_bookmark

$ echo brand_new_file > brand_new_file
$ git add .
$ git commit -qam "Commit"

# This push is blocked
$ git_client push origin HEAD:master_bookmark/another_master
To https://localhost:$LOCAL_PORT/repos/git/ro/repo.git
! [remote rejected] HEAD -> master_bookmark/another_master (hooks failed:
block_new_bookmark_creations_by_prefix for f53155321de7df9aa68c3b4b418019e612f0fa4b: Creation of bookmark "heads/master_bookmark/another_master" was blocked because its path prefix "heads/master_bookmark" already exists as a bookmark

For more information about hooks and bypassing, refer https://fburl.com/wiki/mb4wtk1j)
error: failed to push some refs to 'https://localhost:$LOCAL_PORT/repos/git/ro/repo.git'
[1]

# Create a new branch and push to it
$ git checkout -b just/some/created/branch
Switched to a new branch 'just/some/created/branch'
$ echo new_content > new_content
$ git add .
$ git commit -qam "New content commit"
$ git_client push origin HEAD:just/some/created/branch
To https://localhost:$LOCAL_PORT/repos/git/ro/repo.git
* [new branch] HEAD -> just/some/created/branch

# Try pushing a path prefix of the branch. This will fail
$ echo more_content > more_content
$ git add .
$ git commit -qam "More new content"
$ git_client push origin HEAD:just/some/created
To https://localhost:$LOCAL_PORT/repos/git/ro/repo.git
! [remote rejected] HEAD -> just/some/created (hooks failed:
block_new_bookmark_creations_by_prefix for 134d5c589615ac5e391391b82f46f3722f89c924: Creation of bookmark "heads/just/some/created" was blocked because it exists as a path prefix of an existing bookmark

For more information about hooks and bypassing, refer https://fburl.com/wiki/mb4wtk1j)
error: failed to push some refs to 'https://localhost:$LOCAL_PORT/repos/git/ro/repo.git'
[1]
$ git_client push origin HEAD:just
To https://localhost:$LOCAL_PORT/repos/git/ro/repo.git
! [remote rejected] HEAD -> just (hooks failed:
block_new_bookmark_creations_by_prefix for 134d5c589615ac5e391391b82f46f3722f89c924: Creation of bookmark "heads/just" was blocked because it exists as a path prefix of an existing bookmark

For more information about hooks and bypassing, refer https://fburl.com/wiki/mb4wtk1j)
error: failed to push some refs to 'https://localhost:$LOCAL_PORT/repos/git/ro/repo.git'
[1]

# Try pushing a prefix of the branch that is not path-prefix. This should work
$ echo yet_more_content > yet_more_content
$ git add .
$ git commit -qam "More new content"
$ git_client push origin HEAD:just/some/cr
To https://localhost:$LOCAL_PORT/repos/git/ro/repo.git
* [new branch] HEAD -> just/some/cr

0 comments on commit 94f4603

Please sign in to comment.