Repository: huggingface/huggingface_hub
Pull request #281 (2021-08-16 13:34:05)

Add Batch Transform section with video to SageMaker documentation
# What does this PR do? 

This PR adds a dedicated section to the Inference Documentation for Batch Transform. It also introduces a new notebook and yt video. 

philschmid: @Pierrci could we deploy this tomorrow 10AM PST/ 4PM EST/ 7PM CET? 

Patch:
diff --git a/docs/sagemaker/inference.md b/docs/sagemaker/inference.md
index 74f1f95a..8b307037 100644
--- a/docs/sagemaker/inference.md
+++ b/docs/sagemaker/inference.md
@@ -22,11 +22,11 @@ huggingface_model = HuggingFaceModel(...).deploy()
   - [Deploy the model directly after training](#deploy-the-model-directly-after-training)
   - [Deploy the model using `model_data`](#deploy-the-model-using-model_data)
 - [Deploy for inference one of the 10,000+ 🤗 Transformers models available in the 🤗 Hub](#deploy-for-inference-one-of-the-10000-%F0%9F%A4%97-transformers-models-available-in-the-%F0%9F%A4%97-hub)
+- [Run a Batch Transform Job using 🤗 Transformers and Amazon SageMaker](#run-a-batch-transform-job-using-🤗-transformers-and-amazon-sagemaker)
 - [Advanced Features](#advanced-features)
   - [Environment variables](#environment-variables)
   - [Creating a Model artifact model.tar.gz for deployment](#creating-a-model-artifact-modeltargz-for-deployment)
   - [User defined code/modules](#user-defined-codemodules)
-  - [Batch transform](#batch-transform)
 - [Additional Resources](#additional-resources)
 
 
@@ -135,7 +135,7 @@ sess = sagemaker.Session()
 
 There are two ways to deploy your Hugging Face model trained in SageMaker. You can either deploy it after your training is finished, or you can deploy it later, using the `model_data` pointing to your saved model on S3.
 
-- [Deploy a Hugging Face Transformer model from S3 to SageMaker for inference](https://github.com/huggingface/notebooks/blob/master/sagemaker/10_deploy_model_from_s3/deploy_transformer_model_from_s3.ipynb)
+- [Notebook Example](https://github.com/huggingface/notebooks/blob/master/sagemaker/10_deploy_model_from_s3/deploy_transformer_model_from_s3.ipynb)
 
 
 ### Deploy the model directly after training
@@ -216,7 +216,7 @@ predictor.delete_endpoint()
 
 <iframe width="700" height="394" src="https://www.youtube.com/embed/l9QZuazbzWM" title="YouTube video player" frameborder="0" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture" allowfullscreen></iframe>
 
-- [Deploy one of the 10 000+ Hugging Face Transformers to Amazon SageMaker for Inference](https://github.com/huggingface/notebooks/blob/master/sagemaker/11_deploy_model_from_hf_hub/deploy_transformer_model_from_hf_hub.ipynb)
+- [Notebook Example](https://github.com/huggingface/notebooks/blob/master/sagemaker/11_deploy_model_from_hf_hub/deploy_transformer_model_from_hf_hub.ipynb)
 
 To deploy a model directly from the 🤗 Hub to SageMaker, we need to define 2 environment variables when creating the HuggingFaceModel. We need to define:
 
@@ -268,6 +268,82 @@ predictor.delete_endpoint()
 
 ---
 
+
+## Run a Batch Transform Job using 🤗 Transformers and Amazon SageMaker
+
+<iframe width="700" height="394" src="https://www.youtube.com/embed/lnTixz0tUBg" title="YouTube video player" frameborder="0" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture" allowfullscreen></iframe>
+
+- [Notebook Example](https://github.com/huggingface/notebooks/blob/master/sagemaker/12_batch_transform_inference/sagemaker-notebook.ipynb)
+
+After you train a model, you can use [Amazon SageMaker Batch Transform](https://docs.aws.amazon.com/sagemaker/latest/dg/how-it-works-batch.html) to perform inferences with the model. In Batch Transform you provide your inference data as an S3 URI and SageMaker will take care of downloading it, running the prediction, and uploading the results afterward to S3 again. You can find more documentation for Batch Transform [here](https://docs.aws.amazon.com/sagemaker/latest/dg/batch-transform.html)
+
+>**The Hugging Face Inference DLC currently only supports `.jsonl` for batch transform, due to the complex structure of textual data.** 
+
+_**NOTE**: While preprocessing, you need to make sure that your `inputs` fit the `max_length`of the model._
+
+If you trained the model using the `HuggingFace` estimator, you can invoke `transformer()` method to create a transform job for a model based on the training job.
+
+```python
+batch_job = huggingface_estimator.transformer(
+    instance_count=1,
+    instance_type='ml.p3.2xlarge',
+    strategy='SingleRecord')
+
+
+batch_job.transform(
+    data='s3://s3-uri-to-batch-data',
+    content_type='application/json',    
+    split_type='Line')
+```
+
+For more details about what can be specified here, see [API docs](https://sagemaker.readthedocs.io/en/stable/overview.html#sagemaker-batch-transform).
+
+If you want to run your Batch Transform Job later or with a model from hf.co/models you can do this by creating a `HuggingFaceModel` instance and then using the `transformer()` method.
+
+```python
+from sagemaker.huggingface.model import HuggingFaceModel
+
+# Hub Model configuration. <https://huggingface.co/models>
+hub = {
+	'HF_MODEL_ID':'distilbert-base-uncased-finetuned-sst-2-english',
+	'HF_TASK':'text-classification'
+}
+
+# create Hugging Face Model Class
+huggingface_model = HuggingFaceModel(
+   env=hub, # configuration for loading model from Hub
+   role=role, # iam role with permissions to create an Endpoint
+   transformers_version="4.6", # transformers version used
+   pytorch_version="1.7", # pytorch version used
+       py_version='py36', # python version used
+)
+
+# create Transformer to run our batch job
+batch_job = huggingface_model.transformer(
+    instance_count=1,
+    instance_type='ml.p3.2xlarge',
+    strategy='SingleRecord')
+
+# starts batch transform job and uses s3 data as input
+batch_job.transform(
+    data='s3://sagemaker-s3-demo-test/samples/input.jsonl',
+    content_type='application/json',    
+    split_type='Line')
+```
+
+The `input.jsonl` looks like this:
+
+```jsonl
+{"inputs":"this movie is terrible"}
+{"inputs":"this movie is amazing"}
+{"inputs":"SageMaker is pretty cool"}
+{"inputs":"SageMaker is pretty cool"}
+{"inputs":"this movie is terrible"}
+{"inputs":"this movie is amazing"}
+```
+
+---
+
 ## Advanced Features
 
 ### Environment variables
@@ -402,72 +478,6 @@ def transform_fn(model, input_data, content_type, accept):
 ```
 
 
-### Batch Transform
-
-After you train a model, you can use [Amazon SageMaker Batch Transform](https://docs.aws.amazon.com/sagemaker/latest/dg/how-it-works-batch.html) to perform inferences with the model. In Batch Transform you provide your inference data as a S3 uri and SageMaker will care of downloading it, running the prediction and uploading the results afterwards to S3 again. You can find more documentation for Batch Transform [here](https://docs.aws.amazon.com/sagemaker/latest/dg/batch-transform.html)
-
-If you trained the model using the `HuggingFace` estimator, you can invoke `transformer()` method to create a transform job for a model based on the training job.
-
-```python
-batch_job = huggingface_estimator.transformer(
-    instance_count=1,
-    instance_type='ml.c5.2xlarge',
-    strategy='SingleRecord')
-
-
-batch_job.transform(
-    data='s3://s3-uri-to-batch-data',
-    content_type='application/json',    
-    split_type='Line')
-```
-
-For more details about what can be specified here, see [API docs](https://sagemaker.readthedocs.io/en/stable/overview.html#sagemaker-batch-transform).
-
-If you want to run your Batch Transform Job later or with a model from hf.co/models you can do this by creating a `HuggingFaceModel` instance and then using the `transformer()` method.
-
-```python
-from sagemaker.huggingface.model import HuggingFaceModel
-
-# Hub Model configuration. <https://huggingface.co/models>
-hub = {
-	'HF_MODEL_ID':'distilbert-base-uncased-finetuned-sst-2-english',
-	'HF_TASK':'text-classification'
-}
-
-# create Hugging Face Model Class
-huggingface_model = HuggingFaceModel(
-   env=hub, # configuration for loading model from Hub
-   role=role, # iam role with permissions to create an Endpoint
-   transformers_version="4.6", # transformers version used
-   pytorch_version="1.7", # pytorch version used
-       py_version='py36', # python version used
-)
-
-# create Transformer to run our batch job
-batch_job = huggingface_model.transformer(
-    instance_count=1,
-    instance_type='ml.c5.2xlarge',
-    strategy='SingleRecord')
-
-# starts batch transform job and uses s3 data as input
-batch_job.transform(
-    data='s3://sagemaker-s3-demo-test/samples/input.jsonl',
-    content_type='application/json',    
-    split_type='Line')
-```
-
-The `input.jsonl` looks like this.
-
-```jsonl
-{"inputs":"this movie is terrible"}
-{"inputs":"this movie is amazing"}
-{"inputs":"SageMaker is pretty cool"}
-{"inputs":"SageMaker is pretty cool"}
-{"inputs":"this movie is terrible"}
-{"inputs":"this movie is amazing"}
-```
-
-
 ## Additional Resources
 
 - [Announcement Blog Post](https://huggingface.co/blog/the-partnership-amazon-sagemaker-and-hugging-face)
diff --git a/src/huggingface_hub/commands/user.py b/src/huggingface_hub/commands/user.py
index 1da7debe..1755033a 100644
--- a/src/huggingface_hub/commands/user.py
+++ b/src/huggingface_hub/commands/user.py
@@ -117,6 +117,27 @@ def tabulate(rows: List[List[Union[str, int]]], headers: List[str]) -> str:
     return "\n".join(lines)
 
 
+def currently_setup_credential_helpers(directory=None) -> List[str]:
+    try:
+        output = subprocess.run(
+            "git config --list".split(),
+            stderr=subprocess.PIPE,
+            stdout=subprocess.PIPE,
+            encoding="utf-8",
+            check=True,
+            cwd=directory,
+        ).stdout.split("\n")
+
+        current_credential_helpers = []
+        for line in output:
+            if "credential.helper" in line:
+                current_credential_helpers.append(line.split("=")[-1])
+    except subprocess.CalledProcessError as exc:
+        raise EnvironmentError(exc.stderr)
+
+    return current_credential_helpers
+
+
 class BaseUserCommand:
     def __init__(self, args):
         self.args = args
@@ -147,6 +168,16 @@ class LoginCommand(BaseUserCommand):
         HfFolder.save_token(token)
         print("Login successful")
         print("Your token has been saved to", HfFolder.path_token)
+        helpers = currently_setup_credential_helpers()
+
+        if "store" not in helpers:
+            print(
+                ANSI.red(
+                    "Authenticated through git-crendential store but this isn't the helper defined on your machine.\nYou "
+                    "will have to re-authenticate when pushing to the Hugging Face Hub. Run the following command in your "
+                    "terminal to set it as the default\n\ngit config --global credential.helper store"
+                )
+            )
 
 
 class WhoamiCommand(BaseUserCommand):
diff --git a/src/huggingface_hub/hf_api.py b/src/huggingface_hub/hf_api.py
index b730bfbc..b1a32e19 100644
--- a/src/huggingface_hub/hf_api.py
+++ b/src/huggingface_hub/hf_api.py
@@ -16,11 +16,12 @@
 
 import os
 import re
+import subprocess
 import sys
 import warnings
 from io import BufferedIOBase, RawIOBase
 from os.path import expanduser
-from typing import BinaryIO, Dict, Iterable, List, Optional, Union
+from typing import BinaryIO, Dict, Iterable, List, Optional, Tuple, Union
 
 import requests
 from requests.exceptions import HTTPError
@@ -224,6 +225,79 @@ class DatasetInfo:
         return r
 
 
+def write_to_credential_store(username: str, password: str):
+    with subprocess.Popen(
+        "git credential-store store".split(),
+        stdin=subprocess.PIPE,
+        stdout=subprocess.PIPE,
+        stderr=subprocess.STDOUT,
+    ) as process:
+        input_username = f"username={username.lower()}"
+        input_password = f"password={password}"
+
+        process.stdin.write(
+            f"url={ENDPOINT}\n{input_username}\n{input_password}\n\n".encode("utf-8")
+        )
+        process.stdin.flush()
+
+
+def read_from_credential_store(
+    username=None,
+) -> Tuple[Union[str, None], Union[str, None]]:
+    """
+    Reads the credential store relative to huggingface.co. If no `username` is specified, will read the first
+    entry for huggingface.co, otherwise will read the entry corresponding to the username specified.
+
+    The username returned will be all lowercase.
+    """
+    with subprocess.Popen(
+        "git credential-store get".split(),
+        stdin=subprocess.PIPE,
+        stdout=subprocess.PIPE,
+        stderr=subprocess.STDOUT,
+    ) as process:
+        standard_input = f"url={ENDPOINT}\n"
+
+        if username is not None:
+            standard_input += f"username={username.lower()}\n"
+
+        standard_input += "\n"
+
+        process.stdin.write(standard_input.encode("utf-8"))
+        process.stdin.flush()
+        output = process.stdout.read()
+        output = output.decode("utf-8")
+
+    if len(output) == 0:
+        return None, None
+
+    username, password = [line for line in output.split("\n") if len(line) != 0]
+    return username.split("=")[1], password.split("=")[1]
+
+
+def erase_from_credential_store(username=None):
+    """
+    Erases the credential store relative to huggingface.co. If no `username` is specified, will erase the first
+    entry for huggingface.co, otherwise will erase the entry corresponding to the username specified.
+    """
+    with subprocess.Popen(
+        "git credential-store erase".split(),
+        stdin=subprocess.PIPE,
+        stdout=subprocess.PIPE,
+        stderr=subprocess.STDOUT,
+    ) as process:
+        standard_input = f"url={ENDPOINT}\n"
+
+        if username is not None:
+            standard_input += f"username={username.lower()}\n"
+
+        standard_input += "\n"
+
+        process.stdin.write(standard_input.encode("utf-8"))
+        print(standard_input)
+        process.stdin.flush()
+
+
 class HfApi:
     def __init__(self, endpoint=None):
         self.endpoint = endpoint if endpoint is not None else ENDPOINT
@@ -240,6 +314,8 @@ class HfApi:
         r = requests.post(path, json={"username": username, "password": password})
         r.raise_for_status()
         d = r.json()
+
+        write_to_credential_store(username, password)
         return d["token"]
 
     def whoami(self, token: str) -> Dict:
@@ -258,6 +334,9 @@ class HfApi:
         """
         Call HF API to log out.
         """
+        username = self.whoami(token)["name"]
+        erase_from_credential_store(username)
+
         path = "{}/api/logout".format(self.endpoint)
         r = requests.post(path, headers={"authorization": "Bearer {}".format(token)})
         r.raise_for_status()
diff --git a/src/huggingface_hub/repository.py b/src/huggingface_hub/repository.py
index cda08e79..2d7b2769 100644
--- a/src/huggingface_hub/repository.py
+++ b/src/huggingface_hub/repository.py
@@ -300,11 +300,22 @@ class Repository:
                     "If not specifying `clone_from`, you need to pass Repository a valid git clone."
                 )
 
-        # overrides .git config if user and email is provided.
+        if self.huggingface_token is not None and (
+            git_email is None or git_user is None
+        ):
+            user = HfApi().whoami(self.huggingface_token)
+
+            if git_email is None:
+                git_email = user["email"]
+
+            if git_user is None:
+                git_user = user["fullname"]
+
         if git_user is not None or git_email is not None:
             self.git_config_username_and_email(git_user, git_email)
 
         self.lfs_enable_largefiles()
+        self.git_credential_helper_store()
 
         if revision is not None:
             self.git_checkout(revision, create_branch_ok=True)
@@ -501,6 +512,22 @@ class Repository:
         except subprocess.CalledProcessError as exc:
             raise EnvironmentError(exc.stderr)
 
+    def git_credential_helper_store(self):
+        """
+        sets the git credential helper to `store`
+        """
+        try:
+            subprocess.run(
+                ["git", "config", "credential.helper", "store"],
+                stderr=subprocess.PIPE,
+                stdout=subprocess.PIPE,
+                check=True,
+                encoding="utf-8",
+                cwd=self.local_dir,
+            )
+        except subprocess.CalledProcessError as exc:
+            raise EnvironmentError(exc.stderr)
+
     def git_head_hash(self) -> str:
         """
         Get commit sha on top of HEAD.
diff --git a/widgets/src/lib/InferenceWidget/InferenceWidget.svelte b/widgets/src/lib/InferenceWidget/InferenceWidget.svelte
index fa4c3ec8..94c4e900 100644
--- a/widgets/src/lib/InferenceWidget/InferenceWidget.svelte
+++ b/widgets/src/lib/InferenceWidget/InferenceWidget.svelte
@@ -14,6 +14,7 @@
 	import SummarizationWidget from "./widgets/SummarizationWidget/SummarizationWidget.svelte";
 	import TableQuestionAnsweringWidget from "./widgets/TableQuestionAnsweringWidget/TableQuestionAnsweringWidget.svelte";
 	import TextGenerationWidget from "./widgets/TextGenerationWidget/TextGenerationWidget.svelte";
+	import TextToImageWidget from "./widgets/TextToImageWidget/TextToImageWidget.svelte";
 	import TextToSpeechWidget from "./widgets/TextToSpeechWidget/TextToSpeechWidget.svelte";
 	import TokenClassificationWidget from "./widgets/TokenClassificationWidget/TokenClassificationWidget.svelte";
 	import StructuredDataClassificationWidget from "./widgets/StructuredDataClassificationWidget/StructuredDataClassificationWidget.svelte";
@@ -48,6 +49,7 @@
 		"text-classification": FillMaskWidget,
 		"text-generation": TextGenerationWidget,
 		"token-classification": TokenClassificationWidget,
+		"text-to-image": TextToImageWidget,
 		"text-to-speech": TextToSpeechWidget,
 		translation: TextGenerationWidget,
 		"structured-data-classification": StructuredDataClassificationWidget,
diff --git a/widgets/src/lib/InferenceWidget/widgets/TextToImageWidget/TextToImageWidget.svelte b/widgets/src/lib/InferenceWidget/widgets/TextToImageWidget/TextToImageWidget.svelte
new file mode 100644
index 00000000..ee5fc556
--- /dev/null
+++ b/widgets/src/lib/InferenceWidget/widgets/TextToImageWidget/TextToImageWidget.svelte
@@ -0,0 +1,128 @@
+<script>
+	import type { WidgetProps } from "../../shared/types";
+
+	import { onMount } from "svelte";
+	import WidgetQuickInput from "../../shared/WidgetQuickInput/WidgetQuickInput.svelte";
+	import WidgetWrapper from "../../shared/WidgetWrapper/WidgetWrapper.svelte";
+	import {
+		getDemoInputs,
+		getResponse,
+		getSearchParams,
+		updateUrl,
+	} from "../../shared/helpers";
+
+	export let apiToken: WidgetProps["apiToken"];
+	export let apiUrl: WidgetProps["apiUrl"];
+	export let callApiOnMount: WidgetProps["callApiOnMount"];
+	export let model: WidgetProps["model"];
+	export let noTitle: WidgetProps["noTitle"];
+	export let shouldUpdateUrl: WidgetProps["shouldUpdateUrl"];
+
+	let computeTime = "";
+	let error: string = "";
+	let isLoading = false;
+	let modelLoading = {
+		isLoading: false,
+		estimatedTime: 0,
+	};
+	let output = "";
+	let outputJson = "";
+	let text = "";
+
+	onMount(() => {
+		const [textParam] = getSearchParams(["text"]);
+		if (textParam) {
+			text = textParam;
+			getOutput();
+		} else {
+			const [demoText] = getDemoInputs(model, ["text"]);
+			text = (demoText as string) ?? "";
+			if (text && callApiOnMount) {
+				getOutput();
+			}
+		}
+	});
+
+	async function getOutput(withModelLoading = false) {
+		const trimmedText = text.trim();
+
+		if (!trimmedText) {
+			error = "You need to input some text";
+			output = "";
+			return;
+		}
+
+		if (shouldUpdateUrl) {
+			updateUrl({ text: trimmedText });
+		}
+
+		const requestBody = { inputs: trimmedText };
+
+		isLoading = true;
+
+		const res = await getResponse(
+			apiUrl,
+			model.modelId,
+			requestBody,
+			apiToken,
+			parseOutput,
+			withModelLoading
+		);
+
+		isLoading = false;
+		// Reset values
+		computeTime = "";
+		error = "";
+		modelLoading = { isLoading: false, estimatedTime: 0 };
+		output = "";
+		outputJson = "";
+
+		if (res.status === "success") {
+			computeTime = res.computeTime;
+			output = res.output;
+			outputJson = res.outputJson;
+		} else if (res.status === "loading-model") {
+			modelLoading = {
+				isLoading: true,
+				estimatedTime: res.estimatedTime,
+			};
+			getOutput(true);
+		} else if (res.status === "error") {
+			error = res.error || `Error encountered on input "${trimmedText}"`;
+		}
+	}
+
+	function parseOutput(body: unknown): string {
+		if (body && typeof body === "object" && body instanceof Blob) {
+			return URL.createObjectURL(body);
+		}
+		throw new TypeError(
+			"Invalid output: output must be of type object & of instance Blob"
+		);
+	}
+</script>
+
+<WidgetWrapper
+	{apiUrl}
+	{computeTime}
+	{error}
+	{model}
+	{modelLoading}
+	{noTitle}
+	{outputJson}
+>
+	<svelte:fragment slot="top">
+		<form>
+			<WidgetQuickInput
+				bind:value={text}
+				{isLoading}
+				onClickSubmitBtn={() => getOutput()}
+			/>
+		</form>
+	</svelte:fragment>
+	<svelte:fragment slot="bottom">
+		{#if output.length}
+			<img class="mt-4 object-contain" src={output} alt="" />
+		{/if}
+	</svelte:fragment>
+</WidgetWrapper>
diff --git a/widgets/src/routes/index.svelte b/widgets/src/routes/index.svelte
index c03fec90..6c425c49 100644
--- a/widgets/src/routes/index.svelte
+++ b/widgets/src/routes/index.svelte
@@ -136,6 +136,10 @@
 			pipeline_tag: "conversational",
 			widgetData: [{ text: "Hey my name is Julien! How are you?" }],
 		},
+		{
+			modelId: "osanseviero/BigGAN-deep-128",
+			pipeline_tag: "text-to-image",
+		},
 		{
 			modelId: "julien-c/kan-bayashi_csmsc_tacotron2",
 			pipeline_tag: "text-to-speech",


Test patch:
diff --git a/tests/test_hf_api.py b/tests/test_hf_api.py
index ffb941c6..a66ac12b 100644
--- a/tests/test_hf_api.py
+++ b/tests/test_hf_api.py
@@ -29,6 +29,8 @@ from huggingface_hub.hf_api import (
     HfFolder,
     ModelInfo,
     RepoObj,
+    erase_from_credential_store,
+    read_from_credential_store,
     repo_type_and_id_from_hf_id,
 )
 from requests.exceptions import HTTPError
@@ -67,6 +69,13 @@ class HfApiCommonTest(unittest.TestCase):
 
 
 class HfApiLoginTest(HfApiCommonTest):
+    def setUp(self) -> None:
+        erase_from_credential_store(USER)
+
+    @classmethod
+    def tearDownClass(cls) -> None:
+        cls._api.login(username=USER, password=PASS)
+
     def test_login_invalid(self):
         with self.assertRaises(HTTPError):
             self._api.login(username=USER, password="fake")
@@ -75,6 +84,13 @@ class HfApiLoginTest(HfApiCommonTest):
         token = self._api.login(username=USER, password=PASS)
         self.assertIsInstance(token, str)
 
+    def test_login_git_credentials(self):
+        self.assertTupleEqual(read_from_credential_store(USER), (None, None))
+        self._api.login(username=USER, password=PASS)
+        self.assertTupleEqual(read_from_credential_store(USER), (USER.lower(), PASS))
+        erase_from_credential_store(username=USER)
+        self.assertTupleEqual(read_from_credential_store(USER), (None, None))
+
 
 class HfApiCommonTestWithLogin(HfApiCommonTest):
     @classmethod
diff --git a/tests/test_repository.py b/tests/test_repository.py
index 0b78b091..2208334b 100644
--- a/tests/test_repository.py
+++ b/tests/test_repository.py
@@ -22,6 +22,7 @@ import unittest
 from io import BytesIO
 
 import requests
+from huggingface_hub.commands.user import currently_setup_credential_helpers
 from huggingface_hub.hf_api import HfApi
 from huggingface_hub.repository import (
     Repository,
@@ -815,6 +816,76 @@ class RepositoryOfflineTest(RepositoryCommonTest):
         files = os.listdir(current_head_repo.local_dir)
         self.assertIn("file.txt", files)
 
+    def test_repo_user(self):
+        api = HfApi(endpoint=ENDPOINT_STAGING)
+        token = api.login(USER, PASS)
+
+        repo = Repository(WORKING_REPO_DIR, use_auth_token=token)
+        user = api.whoami(token)
+
+        username = subprocess.run(
+            ["git", "config", "user.name"],
+            stderr=subprocess.PIPE,
+            stdout=subprocess.PIPE,
+            check=True,
+            encoding="utf-8",
+            cwd=repo.local_dir,
+        ).stdout.strip()
+        email = subprocess.run(
+            ["git", "config", "user.email"],
+            stderr=subprocess.PIPE,
+            stdout=subprocess.PIPE,
+            check=True,
+            encoding="utf-8",
+            cwd=repo.local_dir,
+        ).stdout.strip()
+
+        self.assertEqual(username, user["fullname"])
+        self.assertEqual(email, user["email"])
+
+    def test_repo_passed_user(self):
+        api = HfApi(endpoint=ENDPOINT_STAGING)
+        token = api.login(USER, PASS)
+        repo = Repository(
+            WORKING_REPO_DIR,
+            git_user="RANDOM_USER",
+            git_email="EMAIL@EMAIL.EMAIL",
+            use_auth_token=token,
+        )
+        username = subprocess.run(
+            ["git", "config", "user.name"],
+            stderr=subprocess.PIPE,
+            stdout=subprocess.PIPE,
+            check=True,
+            encoding="utf-8",
+            cwd=repo.local_dir,
+        ).stdout.strip()
+        email = subprocess.run(
+            ["git", "config", "user.email"],
+            stderr=subprocess.PIPE,
+            stdout=subprocess.PIPE,
+            check=True,
+            encoding="utf-8",
+            cwd=repo.local_dir,
+        ).stdout.strip()
+
+        self.assertEqual(username, "RANDOM_USER")
+        self.assertEqual(email, "EMAIL@EMAIL.EMAIL")
+
+    def test_correct_helper(self):
+        subprocess.run(
+            ["git", "config", "--global", "credential.helper", "get"],
+            stderr=subprocess.PIPE,
+            stdout=subprocess.PIPE,
+            check=True,
+            encoding="utf-8",
+        )
+        repo = Repository(WORKING_REPO_DIR)
+        self.assertListEqual(
+            currently_setup_credential_helpers(repo.local_dir), ["get", "store"]
+        )
+        self.assertEqual(currently_setup_credential_helpers(), ["get"])
+
 
 class RepositoryDatasetTest(RepositoryCommonTest):
     @classmethod