Repository: runatlantis/atlantis (language: go)

Issue:
WriteGitCreds only writes a single line
<!--- Please keep this note for the community --->

### Community Note

* Please vote on this issue by adding a 👍 [reaction](https://blog.github.com/2016-03-10-add-reactions-to-pull-requests-issues-and-comments/) to the original issue to help the community and maintainers prioritize this request. Searching for pre-existing feature requests helps us consolidate datapoints for identical requirements into a single place, thank you!
* Please do not leave "+1" or other comments that do not add relevant new information or questions, they generate extra noise for issue followers and do not help prioritize the request.
* If you are interested in working on this issue or have submitted a pull request, please leave a comment.

<!--- Thank you for keeping this note for the community --->

---


### Overview of the Issue

I've been trying to setup atlantis to work with both Bitbucket and Github since we are trying to migrate away from Bitbucket. Setting up atlantis as github app I ran into an error. 

```
git clone depth ....
 fatal: could not read Username for 'https://github.com': No such device or address
 ```




### Reproduction Steps

<!--- 
In order to effectively and quickly resolve the issue, please provide exact steps that allow us the reproduce the problem. If no steps are provided, then it will likely take longer to get the issue resolved.
--->


To reproduce setup atlantis with 

```

            - name: ATLANTIS_GH_APP_ID
              value: '376225'
            - name: ATLANTIS_GH_APP_KEY
              valueFrom:
                secretKeyRef:
                  name: atlantis-vcs
                  key: ATLANTIS_GH_APP_KEY
            - name: ATLANTIS_WRITE_GIT_CREDS
              value: "true"
            - name: ATLANTIS_GH_WEBHOOK_SECRET
              valueFrom:
                secretKeyRef:
                  name: atlantis-vcs
                  key: ATLANTIS_GH_WEBHOOK_SECRET
            - name: ATLANTIS_GH_ORG
              value: some-org
            ### Bitbucket Config ###
            - name: ATLANTIS_BITBUCKET_USER
              value: GCP-Infra-Team-Bot
            - name: ATLANTIS_BITBUCKET_TOKEN
              valueFrom:
                secretKeyRef:
                  name: atlantis-vcs
                  key: ATLANTIS_BITBUCKET_TOKEN
```

What will happen is that the server will see that a Bitbucket User and Token are set and write to the `.git-credentials` file the authenticated url for bitbucket but then won't write the line for the github.

I feel like this is a bug. I had a look a the code I think it might be here. https://github.com/runatlantis/atlantis/blob/main/server/events/vcs/git_cred_writer.go#L40

Since the github credentials are never written but the file already exists it will never write the credential. If I delete the file while the server is running it will then eventually write the file with the github credential but without the Bitbucket one.

Is this intended behavior or a bug?




Discussion / hints:

Fix patch:
diff --git a/server/events/vcs/git_cred_writer.go b/server/events/vcs/git_cred_writer.go
index 6d6cf85317..eca5dc00d7 100644
--- a/server/events/vcs/git_cred_writer.go
+++ b/server/events/vcs/git_cred_writer.go
@@ -38,11 +38,23 @@ func WriteGitCreds(gitUser string, gitToken string, gitHostname string, home str
 		}
 
 		if ghAccessToken {
-			// Need to replace the line.
-			if err := fileLineReplace(config, gitUser, gitHostname, credsFile); err != nil {
-				return errors.Wrap(err, "replacing git credentials line for github app")
+			hasGHToken, err := fileHasGHToken(gitUser, gitHostname, credsFile)
+			if err != nil {
+				return err
+			}
+			if hasGHToken {
+				// Need to replace the line.
+				if err := fileLineReplace(config, gitUser, gitHostname, credsFile); err != nil {
+					return errors.Wrap(err, "replacing git credentials line for github app")
+				}
+				logger.Info("updated git app credentials in %s", credsFile)
+			} else {
+				if err := fileAppend(config, credsFile); err != nil {
+					return err
+				}
+				logger.Info("wrote git credentials to %s", credsFile)
 			}
-			logger.Info("updated git app credentials in %s", credsFile)
+
 		} else {
 			// Otherwise we need to append the line.
 			if err := fileAppend(config, credsFile); err != nil {
@@ -113,3 +125,17 @@ func fileLineReplace(line, user, host, filename string) error {
 
 	return os.WriteFile(filename, []byte(toWrite), 0600)
 }
+
+func fileHasGHToken(user, host, filename string) (bool, error) {
+	currContents, err := os.ReadFile(filename) // nolint: gosec
+	if err != nil {
+		return false, err
+	}
+	prevLines := strings.Split(string(currContents), "\n")
+	for _, l := range prevLines {
+		if strings.HasPrefix(l, "https://"+user) && strings.HasSuffix(l, host) {
+			return true, nil
+		}
+	}
+	return false, nil
+}


Test patch:
diff --git a/server/events/vcs/git_cred_writer_test.go b/server/events/vcs/git_cred_writer_test.go
index 082f732475..64e7588672 100644
--- a/server/events/vcs/git_cred_writer_test.go
+++ b/server/events/vcs/git_cred_writer_test.go
@@ -86,6 +86,25 @@ func TestWriteGitCreds_ReplaceApp(t *testing.T) {
 	Equals(t, expContets, string(actContents))
 }
 
+// Test that the github app credential gets added even if there are other credentials.
+func TestWriteGitCreds_AppendAppWhenFileNotEmpty(t *testing.T) {
+	logger := logging.NewNoopLogger(t)
+	tmp := t.TempDir()
+	t.Setenv("HOME", tmp)
+
+	credsFile := filepath.Join(tmp, ".git-credentials")
+	contents := "line1\nhttps://user:token@host.com\nline2"
+	err := os.WriteFile(credsFile, []byte(contents), 0600)
+	Ok(t, err)
+
+	err = vcs.WriteGitCreds("x-access-token", "token", "github.com", tmp, logger, true)
+	Ok(t, err)
+	expContets := "line1\nhttps://user:token@host.com\nline2\nhttps://x-access-token:token@github.com"
+	actContents, err := os.ReadFile(filepath.Join(tmp, ".git-credentials"))
+	Ok(t, err)
+	Equals(t, expContets, string(actContents))
+}
+
 // Test that the github app credentials get updated when cred file is empty.
 func TestWriteGitCreds_AppendApp(t *testing.T) {
 	logger := logging.NewNoopLogger(t)