Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
23 changes: 18 additions & 5 deletions app/controllers/projects_controller.rb
Original file line number Diff line number Diff line change
Expand Up @@ -299,25 +299,37 @@ def sync_journal
return
end

SyncJournalJob.perform_now(@project.id, clear: params[:clear] == "true")
result = SyncJournalJob.perform_now(@project.id, clear: params[:clear] == "true")
audit!("project.journal_synced", target: @project)
redirect_to @project, notice: "Journal synced."

case result
when :invalid_repo
redirect_to @project, alert: "That repo link doesn't look like a GitHub, GitLab, or Codeberg URL."
when :fetch_failed
redirect_to @project, alert: "Couldn't find a JOURNAL.md in that repo. Check the repo link and branch."
else
redirect_to @project, notice: "Journal synced."
end
end

def export_devlogs
authorize @project, :update?

entries = @project.devlogs.order(id: :asc)
md = +"# #{@project.name}\n\n"
md << "#{@project.subtitle}\n\n" if @project.subtitle.present?
md = +"---\n"
md << "title: #{@project.name.to_json}\n"
md << "author: #{@project.user.display_name.to_json}\n"
md << "description: #{@project.subtitle.to_json}\n" if @project.subtitle.present?
md << "created_at: #{@project.created_at.strftime('%Y-%m-%d').to_json}\n"
md << "---\n\n"

entries.each do |entry|
md << "# #{entry.created_at.strftime('%Y-%m-%d')}: #{entry.title}\n\n"
md << "**Total time spent: #{entry.time_spent}**\n\n" if entry.time_spent.present?
md << "#{entry.content}\n\n"
end

send_data md, filename: "#{@project.name.parameterize}-journal.md", type: "text/markdown"
send_data md, filename: "JOURNAL.md", type: "text/markdown"
end

def set_devlog_mode
Expand Down Expand Up @@ -507,6 +519,7 @@ def serialize_project_detail(project)
tags: project.tags,
repo_link: project.repo_link,
journal_branch: project.journal_branch,
journal_parse_failed: project.journal_parse_failed,
status: project.status,
devlog_mode: project.devlog_mode,
uses_ai: project.uses_ai,
Expand Down
59 changes: 58 additions & 1 deletion app/javascript/pages/Projects/Form.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,7 @@ export default function ProjectsForm({
const [macondoImporting, setMacondoImporting] = useState(false)
const [macondoError, setMacondoError] = useState('')
const [showMacondoImport, setShowMacondoImport] = useState(false)
const [showGitSwitchWarning, setShowGitSwitchWarning] = useState(false)
const isBuildReview = project.tier === 'tier_build_review' || !!project.build_review

const form = useForm({
Expand Down Expand Up @@ -475,7 +476,14 @@ export default function ProjectsForm({
<select
id="devlog_mode"
value={form.data.devlog_mode}
onChange={(e) => form.setData('devlog_mode', e.target.value)}
onChange={(e) => {
const value = e.target.value
if (value === 'git' && project.devlog_mode === 'website') {
setShowGitSwitchWarning(true)
return
}
form.setData('devlog_mode', value)
}}
className="w-full bg-[#0e0e0e] border-none rounded-lg px-4 py-3 text-[#e5e2e1] focus:ring-1 focus:ring-[#ca5924]/30"
>
<option value="">Not chosen</option>
Expand All @@ -486,6 +494,55 @@ export default function ProjectsForm({
</div>
)}

{showGitSwitchWarning && (
<div className="fixed inset-0 bg-black/50 flex items-center justify-center z-50 p-4">
<div className="bg-[#1c1b1b] ghost-border max-w-md w-full p-8 space-y-6">
<div>
<h3 className="text-xl font-headline font-bold text-[#e5e2e1] mb-2">Switching to Git Journal</h3>
<p className="text-stone-400 text-sm mb-4">
Git Journal syncs your devlog from a <code className="text-[#ffb595]">JOURNAL.md</code> file in
your repo. If you later resync, any entry that isn't in that file can be cleared from Forge.
</p>
<p className="text-stone-400 text-sm">
Download an export of your current entries, already formatted as a{' '}
<code className="text-[#ffb595]">JOURNAL.md</code>, so you can commit it to your repo and build
from it.
</p>
</div>

{project.id && (
<a
href={`/projects/${project.id}/export_devlogs`}
className="w-full ghost-border bg-[#0e0e0e] hover:bg-[#2a2a2a] text-stone-300 font-headline font-bold py-2 uppercase tracking-wider text-sm transition-colors flex items-center justify-center gap-2"
>
<span className="material-symbols-outlined text-lg">download</span>
Download JOURNAL.md
</a>
)}

<div className="flex gap-3 pt-2">
<button
type="button"
onClick={() => setShowGitSwitchWarning(false)}
className="flex-1 bg-stone-700/40 hover:bg-stone-700/60 text-stone-400 font-headline font-bold py-2 uppercase tracking-wider text-sm transition-colors"
>
Cancel
</button>
<button
type="button"
onClick={() => {
form.setData('devlog_mode', 'git')
setShowGitSwitchWarning(false)
}}
className="flex-1 signature-smolder text-[#4c1a00] font-headline font-bold py-2 uppercase tracking-wider text-sm active:scale-95 transition-transform"
>
Switch to Git Journal
</button>
</div>
</div>
</div>
)}

<div className="flex gap-3 pt-4">
<button
type="submit"
Expand Down
19 changes: 19 additions & 0 deletions app/javascript/pages/Projects/Show.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -1190,6 +1190,24 @@ export default function ProjectsShow({
</div>
</div>

{project.repo_link && project.journal_parse_failed && can.update && (
<div className="ghost-border bg-amber-950/30 border border-amber-700/40 p-4 mb-6 flex items-start gap-2">
<span className="material-symbols-outlined text-amber-500 text-lg shrink-0 mt-0.5">warning</span>
<div className="text-xs">
<p className="text-amber-200 text-sm font-bold mb-1">Couldn't sync JOURNAL.md</p>
<p className="text-amber-100/70">
Forge couldn't find any journal entries to sync. This can happen if the repo link is invalid,{' '}
<code className="text-amber-100">JOURNAL.md</code> is missing, or it doesn't follow the
expected format. Check the{' '}
<a href="/docs/design/journal-format" className="underline hover:text-amber-50">
format guide
</a>{' '}
and re-sync.
</p>
</div>
</div>
)}

{!project.repo_link && can.update && (
<div className="ghost-border bg-[#1c1b1b] p-8 text-center mb-6">
<span className="material-symbols-outlined text-3xl text-stone-700 mb-3">link_off</span>
Expand All @@ -1215,6 +1233,7 @@ export default function ProjectsShow({
Add a <code className="text-[#ffb595]">JOURNAL.md</code> to your repo and click "Sync
JOURNAL.md" above.
</p>
<p className="text-stone-500 text-xs">If journals are missing, check the format guide.</p>
<a
href="/docs/design/journal-format"
className="text-[#ffb595] text-xs hover:underline mt-2 inline-block"
Expand Down
1 change: 1 addition & 0 deletions app/javascript/types/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -102,6 +102,7 @@ export interface ProjectDetail {
tags: string[]
repo_link: string | null
journal_branch: string | null
journal_parse_failed: boolean
status: ProjectStatus
devlog_mode: 'website' | 'git' | null
review_feedback: string | null
Expand Down
34 changes: 30 additions & 4 deletions app/jobs/sync_journal_job.rb
Original file line number Diff line number Diff line change
Expand Up @@ -15,19 +15,43 @@ def self.sync_if_stale(project)
def perform(project_id, clear: false)
project = Project.find(project_id)
project.update_columns(journal_synced_at: Time.current)
return unless project.repo_link.present?
return :no_repo unless project.repo_link.present?

# A plain sync never removes existing entries on failure - only an explicit
# resync (clear) does, unless there's nothing to preserve in the first place.
wipe_on_failure = clear || !project.devlogs.exists?

parsed = parse_repo_url(project.repo_link)
return unless parsed
unless parsed
project.update_column(:journal_parse_failed, true)
project.devlogs.delete_all if wipe_on_failure
return :invalid_repo
end

branch = project.journal_branch.presence

journal_content = fetch_journal(parsed, branch)
return unless journal_content.present?
if journal_content.nil?
project.update_column(:journal_parse_failed, true)
project.devlogs.delete_all if wipe_on_failure
return :fetch_failed
end

if journal_content.blank?
project.update_column(:journal_parse_failed, false)
project.devlogs.delete_all if wipe_on_failure
return :empty
end

entries = parse_journal_entries(journal_content)

return if entries.empty?
if entries.empty?
project.update_column(:journal_parse_failed, true)
project.devlogs.delete_all if wipe_on_failure
return :parse_failed
end

project.update_column(:journal_parse_failed, false)

current_titles = entries.map { |e| e[:title] }
preserved_lapse_urls = {}
Expand Down Expand Up @@ -94,6 +118,8 @@ def perform(project_id, clear: false)
end

author&.apply_streak_freezes!

:synced
end

private
Expand Down
1 change: 1 addition & 0 deletions app/models/project.rb
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@
# hackatime_projects :string default([]), not null, is an Array
# hidden :boolean default(FALSE), not null
# journal_branch :string
# journal_parse_failed :boolean default(FALSE), not null
# journal_synced_at :datetime
# kudos_count :integer default(0), not null
# name :string not null
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
class AddJournalParseFailedToProjects < ActiveRecord::Migration[8.1]
def change
add_column :projects, :journal_parse_failed, :boolean, default: false, null: false
end
end
3 changes: 2 additions & 1 deletion db/schema.rb

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions test/fixtures/projects.yml
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@
# hackatime_projects :string default([]), not null, is an Array
# hidden :boolean default(FALSE), not null
# journal_branch :string
# journal_parse_failed :boolean default(FALSE), not null
# journal_synced_at :datetime
# kudos_count :integer default(0), not null
# name :string not null
Expand Down
1 change: 1 addition & 0 deletions test/models/project_test.rb
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@
# hackatime_projects :string default([]), not null, is an Array
# hidden :boolean default(FALSE), not null
# journal_branch :string
# journal_parse_failed :boolean default(FALSE), not null
# journal_synced_at :datetime
# kudos_count :integer default(0), not null
# name :string not null
Expand Down
Loading