<p align="center">
  <img src="logo.png" alt="GitMo logo" width="220">
</p>

# GitMo

<p align="center">
  <strong>Automatic GitHub sync for local project folders.</strong>
</p>

GitMo is a small Linux-first desktop app for keeping project folders synced to
GitHub automatically.

A poor-man's Google Docs for coding projects, you work in normal files on your computer, and GitMo quietly turns each
selected folder into a GitHub-backed repo with autosaved commits.

## Why

Git is powerful, but it is easy to forget the repetitive parts: create the repo,
set the remote, commit, push, pull, check status, and repeat. GitMo wraps that
workflow in a simple desktop app so local folders can stay backed up and visible
on GitHub without living in the terminal all day.

GitMo is useful when you want:

- Automatic backups of active project folders
- A GitHub repo created from a plain local folder
- Simple clone/setup for repos that exist on GitHub but not locally
- One folder per repo, organized under a GitMo folder
- Outside folders synced without moving them into the GitMo folder
- Lightweight two-way sync for solo work or low-conflict workflows
- A plain-file workflow instead of a cloud document editor

## What It Does

- Lets you choose a GitMo root folder
- Treats direct child folders as possible GitHub repos
- Lets you add outside folders from anywhere on disk
- Lists your GitHub repos beside local folders
- Clones GitHub repos that are missing locally
- Creates GitHub repos for local folders that are missing remotely
- Initializes plain folders as git repos when needed
- Creates a root `README.md` for new folder repos when one does not exist
- Commits and pushes local changes using `GitMo autosave`
- Supports one-way and two-way sync modes
- Provides a status-first dashboard with a ribbon toolbar for common actions
- Provides a cleaner `Manage Repos` screen with GitHub repos and local folders
  shown side by side
- Supports select-all controls for visible GitHub repos and local folders
- Provides guarded local-folder and GitHub-repo deletion actions
- Can start with the desktop session and run in the background
- Supports adjustable UI text size
- Logs activity to `~/.config/gitmo/gitmo.log`

## Interface

GitMo has two main screens after setup.

`Dashboard`

The dashboard is mostly a status screen. The top ribbon contains the main
actions:

- Stop or start sync
- Manage repos
- Run sync now
- Open the local GitMo folder
- Open your GitHub profile
- Run in the background

The dashboard shows the active GitMo folder, sync state, repository count, sync
mode, last sync status, a repository table, and the activity log.

`Manage Repos`

Manage Repos shows two side-by-side panels:

- GitHub repositories
- Local GitMo folders

Use Search to filter the visible rows. Each panel has a `Select All` checkbox
that selects or clears the currently visible rows in that panel. The bottom bar
contains the sync mode selector, delete actions, Cancel, and Apply Selection.

## Sync Modes

`one-way`

Local changes are committed and pushed to GitHub. Remote changes are detected,
but GitMo does not pull them into the local folder.

`two-way`

GitMo fetches remote updates and pulls fast-forward changes. If both local and
remote changed, GitMo pauses that repo and reports a conflict instead of trying
to guess.

## Requirements

- Linux
- Python 3
- Tkinter
- `git`
- A GitHub personal access token with repo access

GitMo uses the GitHub REST API and the local `git` command-line tool.

## Packaging Status

GitMo has initial Debian/Ubuntu packaging under `debian/`.

Maintainer:

- Kyle Benzle <kbe@gmx.us>
- Homepage: <https://github.com/KyleBenzle/GitMo>
- Source: <https://github.com/KyleBenzle/GitMo>
- License: GPL-3.0-or-later

Current package targets:

- Local `.deb` testing
- Launchpad PPA publishing for `apt install gitmo`
- Later Ubuntu/Kubuntu software app listing

Important before official Ubuntu archive submission:

- Add final project/repository URLs and screenshots for software-center listings.

## License

GitMo is released under the GNU General Public License v3.0 or later.
See `LICENSE` for the full license text.

## Run

From this folder:

```bash
python3 app.py
```

Or double-click `GitMo.desktop`.

Some file managers may ask you to trust or allow the launcher the first time.

## How It Works

This section explains the app from the point of view of the code. GitMo has a
small launcher, a Tkinter UI, a config file, a GitHub API wrapper, a git command
wrapper, and a background sync engine.

### 1. Starting the app

The top-level `app.py` file is intentionally tiny:

```python
from gitmo.app import main

if __name__ == "__main__":
    raise SystemExit(main())
```

That means `python3 app.py` immediately hands control to `gitmo/app.py`. Keeping
the launcher tiny is useful for packaging later because the real app logic lives
inside the `gitmo` package.

Inside `gitmo/app.py`, `main()` creates the Tkinter root window and the app
object:

```python
root = tk.Tk()
app = GitMoApp(root)
app.run()
root.mainloop()
```

`root.mainloop()` is Tkinter's event loop. It keeps the window alive and waits
for button clicks, timers, menu actions, and background thread callbacks.

### 2. Loading saved settings

When `GitMoApp` is created, it calls `load_config()` from `gitmo/config.py`.
That reads:

```text
~/.config/gitmo/config.json
```

The important saved values are:

- `github_token` - the GitHub personal access token
- `github_login` and `github_email` - used for Git commits and profile links
- `gitmo_path` - the main folder GitMo watches
- `repos` - the selected repo names, local paths, sync modes, and enabled flags

The config is represented with dataclasses:

```python
@dataclass
class RepoConfig:
    name: str
    local_path: str = ""
    sync_mode: str = "two-way"
    enabled: bool = True
```

A dataclass is a convenient Python way to store simple structured data without
writing a lot of boilerplate code.

### 3. Deciding which screen to show

`GitMoApp.run()` chooses the first screen:

- if a token and GitMo folder are saved, show the Dashboard
- otherwise, show the login/setup flow

The app no longer blocks startup on a GitHub network request. It shows the UI
from saved settings first, then starts loading GitHub repos in the background.
That makes startup feel faster.

### 4. The UI layer

Most interface code is in `gitmo/app.py`. This is the largest file because
Tkinter apps are often built by creating widgets directly in Python.

Important UI methods:

- `show_login_screen()` - asks for a GitHub token
- `show_gitmo_screen()` - lets the user choose the GitMo folder
- `show_dashboard()` - shows sync status, repo table, and activity log
- `show_repo_selection_screen()` - shows the Manage Repos table

Each screen follows the same pattern:

1. set `self.current_screen`
2. clear old widgets with `_clear()`
3. build frames, labels, buttons, tables, and callbacks
4. start or refresh background work if needed

For example, a button is created by passing a function as its command:

```python
self._button(row, "Browse", browse, width=96).pack(side="left")
```

Here `browse` is a nested function. Tkinter calls it later when the user clicks
the button.

### 5. Choosing folders

GitMo uses a small in-app folder picker instead of the native system file
browser. The class is `FolderPickerDialog` in `gitmo/app.py`.

It is a `tk.Toplevel`, which means it is a separate child window. It only lists
folders, not files. It has:

- path entry
- `Go`
- `Home`
- `Up`
- `New Folder`
- `Cancel`
- `Select`

When the user selects a folder, the dialog stores the result:

```python
self.result = path
self.destroy()
```

The main app waits for the dialog to close:

```python
self.root.wait_window(dialog)
return dialog.result
```

### 6. Loading GitHub repos

GitHub calls live in `gitmo/github_api.py`. The main method for the repo manager
is `list_repos()`. It calls GitHub's REST API with `urllib.request`.

The request includes the token in an HTTP header:

```python
"Authorization": f"Bearer {self.token}"
```

The method pages through results 100 at a time:

```python
query={
    "per_page": 100,
    "page": page,
    "visibility": "all",
}
```

GitMo loads GitHub repos in a background thread so the Manage Repos page can
open immediately. The thread does not touch Tkinter directly. Instead, it puts
results into a queue:

```python
self.remote_repos_queue.put((repos, "", refresh_repos_screen))
```

The Tkinter side checks that queue with `root.after(...)`. This matters because
Tkinter widgets should only be updated from the main UI thread.

### 7. Building the Manage Repos table

The Manage Repos table is built from the union of:

- repo names returned by GitHub
- direct child folders inside the GitMo folder
- outside folders manually added by the user
- enabled saved repos from the config

The key code is `_build_repo_catalog()`:

```python
remote_map = {repo.name: repo for repo in remote_repos}
names = sorted(set(remote_map) | set(local_map))
```

That creates one row per name. If a name exists both locally and on GitHub, it is
one row, not two.

Each row becomes a `RepoSelection`:

```python
RepoSelection(
    name=name,
    local_path=local_path,
    exists_local=local_path.exists(),
    exists_remote=remote_repo is not None,
    local_is_repo=self._is_git_repo_fast(local_path),
    clone_url=remote_repo.clone_url if remote_repo else None,
)
```

The green checks and red X marks in the table are UI state. They answer:

- should this repo exist/sync on GitHub?
- should this repo exist/sync locally?

When the user clicks `Apply Selection`, GitMo turns those choices into actions.

### 8. Applying repo choices

`_apply_repo_selection()` reads every selected row.

The four main cases are:

- GitHub checked and Local checked: sync/link both
- GitHub checked and Local unchecked: keep/create GitHub only
- GitHub unchecked and Local checked: keep/create local folder only
- both unchecked: delete/remove everywhere after confirmation

For example, if a repo exists on GitHub but is missing locally, GitMo clones it:

```python
clone_repo(self._authenticated_url(selection.clone_url or ""), repo_path)
self._set_repo_identity(repo_path)
```

If a folder exists locally but GitHub is missing, GitMo creates a GitHub repo:

```python
remote_repo = self.github_client.create_repo(selection.name)
self._connect_local_repo_to_remote(repo_path, remote_repo)
```

Dangerous actions are guarded with confirmation dialogs before delete operations
run.

### 9. Running git commands

Git operations are isolated in `gitmo/git_cli.py`. Most functions call
`run_git()`:

```python
def run_git(args: list[str], cwd: Path | None = None) -> str:
    command = ["git", *args]
    result = subprocess.run(command, cwd=str(cwd), capture_output=True, text=True)
```

This keeps the rest of the app readable. Instead of writing `subprocess.run(...)`
everywhere, other modules call simple helpers:

- `clone_repo(...)`
- `init_repo(...)`
- `set_remote_url(...)`
- `commit(...)`
- `push(...)`
- `fetch(...)`
- `pull_ff_only(...)`
- `ahead_behind(...)`

The git wrapper also sets:

```python
GIT_TERMINAL_PROMPT=0
```

That prevents git from hanging forever waiting for terminal input that a desktop
app cannot provide.

### 10. The background sync engine

The sync loop lives in `gitmo/sync_engine.py`. The UI creates it like this:

```python
self.sync_engine = SyncEngine(self.config, self._enqueue_log)
```

The sync engine receives:

- the app config
- a logger callback

It runs in a daemon thread:

```python
self._thread = threading.Thread(target=self._run_loop, daemon=True)
self._thread.start()
```

Every loop checks each enabled repo:

```python
for repo_name, repo_config in self.config.repos.items():
    if not repo_config.enabled:
        continue
```

Then it decides whether to run one-way or two-way sync.

### 11. One-way sync

One-way mode means local changes go to GitHub, but remote changes are not pulled
down automatically.

The engine still fetches remote state:

```python
fetch(repo_path)
ahead, behind = ahead_behind(repo_path)
```

If the remote is ahead, GitMo warns instead of pulling:

```python
Remote is ahead by N commit(s); one-way mode will not pull.
```

### 12. Two-way sync

Two-way mode fetches GitHub changes and pulls only when it is safe.

The key decision is:

```python
if ahead > 0 and behind > 0:
    state = "conflict"
```

That means local Git history and remote Git history both changed. GitMo does not
try to guess which side should win. It marks a conflict and waits for manual git
resolution.

If only remote is ahead:

```python
pull_ff_only(repo_path)
```

`--ff-only` means Git will only move forward cleanly. It will not create a merge
commit or try a complex merge.

### 13. Autosave commits

GitMo watches file modification times in each repo. It ignores `.git` internals:

```python
for path in repo_path.rglob("*"):
    if ".git" in path.parts:
        continue
```

When files changed, GitMo waits for a debounce period before committing:

```python
DEBOUNCE_SECONDS = 60
```

This prevents a commit on every tiny file write. After things are quiet long
enough, GitMo commits with:

```python
AUTOSAVE_MESSAGE = "GitMo autosave"
```

Then it pushes to GitHub.

### 14. Logging

GitMo writes activity to:

```text
~/.config/gitmo/gitmo.log
```

The UI passes `_enqueue_log` into the sync engine. That is an example of a
callback: the sync engine does not need to know about Tkinter or log files. It
just calls the function it was given.

### 15. Running in the background

When the user chooses to keep GitMo running, the app hides/minimizes the window
and keeps the sync engine alive.

GitMo also tries two user-visible signals:

- a desktop notification via `notify-send`
- an optional GTK status icon when the desktop supports it

The tray icon path is deliberately defensive. If the desktop does not support
it, GitMo logs the failure and keeps running instead of crashing.

## Development

GitMo is intentionally small and mostly standard-library Python.

Useful local checks:

```bash
python3 -m py_compile app.py gitmo/*.py
python3 app.py
```

The main modules are:

- `gitmo/app.py` - Tkinter UI, setup screens, dashboard, repo manager, tray/background behavior
- `gitmo/sync_engine.py` - polling sync loop and one-way/two-way sync decisions
- `gitmo/git_cli.py` - small wrappers around the local `git` command
- `gitmo/github_api.py` - GitHub REST API calls
- `gitmo/config.py` - saved settings under `~/.config/gitmo/`

Generated Python bytecode (`__pycache__`, `*.pyc`) is not source and should not
be committed.

## Packaging Direction

The current app runs directly from source. The next packaging step is a local
`.deb` that installs:

- the `gitmo` Python package
- a `/usr/bin/gitmo` launcher
- the desktop entry
- the app icon
- Linux tray/indicator dependencies where available

The project metadata in `pyproject.toml` is a small first step toward that
installable package.

## First Setup

1. Start GitMo.
2. Paste a GitHub personal access token.
3. Choose your GitMo folder.
4. Open `Manage Repos`.
5. Select the folders/repos you want GitMo to manage.
6. Choose `one-way` or `two-way`.
7. Click `Apply Selection`.

After that, GitMo can run in the background and sync selected repos.

## Ribbon Actions

The Dashboard and Manage Repos screens share the same top ribbon:

- `Stop Sync` / `Start Sync`
- `Manage Repos`
- `Sync Now`
- `Open Local Folder`
- `GitHub`
- `Run in Background`

## New Folder Flow

If you add a new folder inside the GitMo folder:

1. Open `Manage Repos`.
2. Select the new folder.
3. Click `Apply Selection`.

GitMo creates a GitHub repo with the folder name, initializes git if needed,
adds a `README.md` if one is missing, commits the folder contents, and pushes
everything to GitHub.

## New GitHub Repo Flow

If you create a repo on GitHub:

1. Open `Manage Repos`.
2. Select the repo.
3. Click `Apply Selection`.

GitMo clones it into the GitMo folder and starts tracking it.

## Outside Folder Flow

If you want to sync a folder that is not inside the GitMo folder:

1. Open `Manage Repos`.
2. Click `Add Outside Folder`.
3. Choose the local folder.
4. Select it in the repo list.
5. Click `Apply Selection`.

If GitHub does not already have a repo with that folder name, GitMo creates one,
adds a `README.md` if needed, commits the folder contents, and pushes it. Future
changes sync from the folder's original location.

## Same Name Local and GitHub Repo

GitMo matches local folders and GitHub repos by name.

If the local folder is already a git repo and a GitHub repo with the same name
exists, GitMo connects the local repo to the GitHub repo as `origin`. It does
not delete either side during selection.

If the local folder is a plain, non-empty folder and a GitHub repo with the same
name exists, GitMo asks before overwriting GitHub. If you confirm, the local
folder is initialized as a git repo and force-pushed to GitHub. If you decline,
that folder is left unmanaged.

If the local folder is empty and the GitHub repo exists, GitMo clones the GitHub
repo into that empty folder.

## Delete Actions

Delete actions are explicit and guarded.

`Delete GitHub Repo`

Deletes only checked rows that have a matching GitHub repo. It does not delete
local folders. GitMo asks for confirmation and requires typing `DELETE`.

`Delete Local Folder`

Deletes only checked rows that have a local folder. It does not delete GitHub
repos. GitMo asks for confirmation and requires typing `DELETE`.

`Stop Sync`

Removes checked rows from GitMo syncing. It does not delete local folders or
GitHub repos.

## App Data

Config:

```text
~/.config/gitmo/config.json
```

Logs:

```text
~/.config/gitmo/gitmo.log
```

Older config files at `~/.config/gitlo/config.json` are read automatically
during the app rename transition.

## Notes

- GitMo is designed for simple folder-to-GitHub sync, not complex team merge
  workflows.
- Two-way sync only pulls fast-forward updates.
- Conflicting local and remote changes require manual git resolution.
- Autosync currently uses a polling loop with a 60 second debounce.
- GitMo uses repository/folder names as the matching key, so one GitHub repo
  name maps to one local folder path.
